Часть 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 для завершения этого плейбука:

 

Рисунок 5.1



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

 

Рисунок 5.2



Как мы можем увидеть из этих простых тестов, 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 '' }}"
 	   

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

 

Рисунок 5.3



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

 

Рисунок 5.4



Как вы можете видеть, мы способны создавать очень лаконичный, но мощный код, который задаёт значения на основе некой переменной Ansible, как мы это наблюдали здесь.

Циклы

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


# data dirs
{% for dir in data_dirs -%}
data_dir = {{ dir }}
{% endfor -%}
 	   
[Совет]Совет

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

В этом примере мы получим по одной строке 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') }}"
 	   

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

 

Рисунок 5.5



Мы можем видеть, что наш оператор 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 -%}
 	   

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

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

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

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

loop.index

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

loop.index0

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

loop.revindex

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

loop.revindex0

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

loop.first

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

loop.last

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

loop.first

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

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


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

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


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

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

 

Рисунок 5.6



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


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

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

 

Рисунок 5.7



Полученный вывод во многом походит на тот что был ранее, но на этот раз за исключением того, что наш шаблон вычисляет будет ли помещена запятая после каждого 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') }}"
 	   

Мы тогда бы получили следующий вывод:

 

Рисунок 5.8



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

arguments

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


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

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

 

Рисунок 5.9



В этом примере мы можем ясно видеть что наш шаблон стрится со значением названия из того аргумента, который получает наш макрос (а не из своих значений).

defaults

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


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

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

 

Рисунок 5.10



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

catch_kwargs

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


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

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

 

Рисунок 5.11



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

catch_varargs

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


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

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

 

Рисунок 5.12



И снова, мы можем видеть, что мы имели возможность перехватить и построить то неожиданное значение, которое было передано в наш макрос, вместо того чтобы возвращать при построении некую ошибку, как это произошло, если бы мы не воспользовались catch_varargs.

caller

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

Давайте обновим наш шаблон чтобы продемонстрировать вариант использования:


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

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

 

Рисунок 5.13



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


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

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

 

Рисунок 5.14



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

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

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

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

Синтаксис

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


{{ my_word | lower }}
 	   

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


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

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


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

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


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

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

 

Рисунок 5.15



Как мы можем здесь наблюдать, в нашей переменной значение слова 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.

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

Хотя множество фильтров предоставляется в 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
 	   

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

 

Рисунок 5.16



Как мы можем наблюдать, наш оператор 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 }}"
 	   

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

 

Рисунок 5.17



Как и ожидалось, мы видим возвращённым весь список, однако его порядок перетасован.

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

Управление настройкой и оркестровка часто ссылаются на имена пути, причём как правило желательно иметь лишь часть всего пути. К примеру, возможно нам требуется значение всего пути к некому файлу, но не само название файла. Или же, возможно, нам требуется выделить из полного пути к какому- то файлу только само название файла, игнорируя предшествующий ему каталог. 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 }}"
 	   

Его вывод показан на снимке экрана внизу:

 

Рисунок 5.18



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

dirname

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

 

Рисунок 5.19



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

expanduser

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


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

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

 

Рисунок 5.20



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

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

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

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


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

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

 

Рисунок 5.21



Здесь мы можем наблюдать что мы успешно считали тот небольшой файл, который мы создали в какой- то переменной и что мы можем видеть значение содержимой переменной, закодированной в виде 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
 	   

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

 

Рисунок 5.22



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

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

Большинство предоставляемых 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 следующие:

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

==

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

!=

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

>

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

<

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

>=

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

<=

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

Если вы пользовались операциями сравнения в каком- то другом языке программирования (обычно в виде некого оператора 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
 	   

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

 

Рисунок 5.23



Здесь мы можем видеть что наше сложное условие вычислило true, а потому была выполнена отладка задачи.

Выводы

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

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

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