Глава 4. Условия управления задачами
{Прим. пер.: рекомендуем сразу обращаться к нашему более полному переводу 3 издания вышедшего в марте 2019 существенно переработанного и дополненного Полного руководства Ansible Джеймса Фримана и Джесса Китинга}
Содержание
Работа Ansible основывается на самом понятии состояний задачи: ok, changed, failed или skipped. Эти состояния определяют должны ли данные задачи исполняться далее на некотором хосте или обработчикам следует получить извещения о некоторых изменениях. Задачи также применяют условные зависимости, которые проверяют полученное состояние предыдущих задач для управления работой.
В данной главе мы изучим способы воздействия на Ansible при определении такого состояния задачи:
-
Управление в случае, когда определён некий отказ
-
Аккуратное восстановление из состояния отказа
-
Контроль того, что определило некое изменение
Большинство поставляемых с Ansible модулей имеют некую опцию, которая устанавливает некую ошибку. Ошибочное
условие сильно зависит от самого модуля и того, что пытается осуществлять данный модуль. Когда какой- то
модуль возвращает некую ошибку, данный хост будет удалён из имеющегося набора доступных хостов, что
предотвращает все последующие задачи или исполнения обработчиков для такого хоста. Далее, определённая
функция ansible-playbook
или исполнение Ansible выйдет с не нулевым значением,
которое указывает отказ. Однако, мы не ограничены неким мнением модуля что имеется какая- то ошибка. Мы можем
игнорировать ошибки или повторно определять такое условие ошибки.
Для игнорирования ошибок применяется некое условие задачи, именуемое ignore_errors
.
Это условие является Булевым, что означает, что его значение дожно быть чем- то, что Ansible понимает как
true
, например, yes
,
on
, true
или
1
(строковое или целое).
Для демонстрации применения ignore_errors
, давайте создадим некий
плейбук с названием errors.yaml
, в котором мы попытаемся запросить некий
несуществующий вебсервер. Обычно это будет некоторой ошибкой и если мы не определили
ignore_errors
, мы получим установленное по умолчанию поведение, то есть
данный хост будет помечен как отказавший и никакие последующие задачи не будут предпринимать свои попытки на
этом хосте. Давайте посмотрим на следующий кусочек кода:
-name: broken website
uri:
url: http://notahost.nodomain
Исполнение такой задачи в том виде, как она представлена, выдаст нам некую ошибку:
Теперь давайте представим себе, что мы не желаем остановки Ansible в данном месте и вместо этого мы хотим
продолжить его. Мы можем добавить своё условие ignore_errors
в нашу задачу
как-то так:
- name: broken website
uri:
url: http://notahost.nodomain
ignore_errors: true
На этот раз, котгда мы исполним этот плейбук, наша ошибка будет проигнорирована, как мы можем увидеть здесь:
Ошибка нашей задачи игнорируется. Все дальнейшие задачи для этого хоста всё ещё будут предприниматься и данный плейбук не зарегистрирует никакие отказавшие хосты.
Задание условия ignore_errors
является достаточно грубым молотом. Все
вырабатываемые в этом применяемом данной задачей модуле ошибки будут проигнорированы. Далее, полученный вывод,
на первый взгляд, всё ещё выглядит как некая ошибка и может смутить некоторого оператора, пытающегося выяснить
истинную причину отказа. Более аккуратным инструментом является условие failed_when
.
Это условие работает как скальпель и позволяет некоторому автору плейбука быть очень точным в отношении того,
что составляет некую ошибку для какой- то задачи. Это условие выполняет некую проверку Булева результата, во
многом схоже с условием when
. Если результаты этого условия в Булевом
выражении истинны, тогда эта задача будет рассматриваться как отказавшая. В противном случае данная задача
будет трактоваться как успешная.
Такое условие failed_when
очень полезно когда применяется совместно с
модулем command
или shell
и
регистрирует свой результат данного исполнения. Многие исполняющиеся программы могут иметь подробные
ненулевые коды выхода, которые обозначают различные моменты, однако, все данные модули рассматривают некий
отличающийся от нуля код выхода как какой- то отказ. Давайте рассмотрим утилиту определённую
iscsiadm
. Эта утилита может применяться для многих целей, относящихся к
iSCSI. Ради целей нашей демонстрации мы применим её для обнаружения любых активных сеансов
iscsi
:
- name: query sessions
command: /sbin/iscsiadm -m session
register: sessions
Если это исполнить в некоторой системе, в которой отсутствуют активные сеансы, мы увидим вывод подобный такому:
Мы можем просто применить своё условие ignore_errors
, однако оно бы
маскировало прочие проблемы с iscsi
; поэтому, вместо него мы желаем
указать Ansible что некий код выхода 21
является допустимым.
При таком окончании мы можем применить свою зарегистрированную переменную для доступа к определённой переменной
rc
, которая содержит код восврата. Вы воспользуемся этим в некотором
выражении failed_when
:
- name: query sessions
command: /sbin/iscsiadm -m session
register: sessions
failed_when: sessions.rc not in (0, 21)
Мы просто устанавливаем, что любой иной код кроме 0
или
21
должен рассматриваться как некий отказ. Давайте рассмотрим свой
новый вывод после такого изменения:
Данный вывод не показывает никаких ошибок и, на самом деле, мы видим некий новый ключ данных в своём
результате - failed_when_result
. Это показывает будет ли наше выражение
failed_when
вычислено как
true
или как false
, причём в
данном случае у нас имеется false
.
Многие инструменты командной строки не имеют подробностей возвращаемых кодов. На самом деле, наиболее
обычным применением является ноль для успеха и какое- либо отличающееся от нуля значение для всех типов отказа.
К счастью, выражение failed_when
не ограничено только значением
кода возврата определённого приложения; оно является свободной формой Булева выражения, которое может получать
доступ к любому виду необходимых данных. Давайте рассмотрим некую другую проблему, связанную с
git
. Мы представим себе какой- то сценарий, при котором мы желаем
быть уверенными, что некое определённое подразделение не присутствует в каком- то сверке
git
. Данная задача предполагает, что некий репозиторий
git
проводит сверку в своём каталоге
/srv/app
. Та команда, которая удалит некое подразделение
git
, это git branch -D
.
Давайте взглянем на следующий фрагмент кода:
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
Замечание | |
---|---|
Модули |
Если мы запустим только эту команду, мы получим некую ошибку с кодом возврата
1
если данное подразделение не существует:
Замечание | |
---|---|
Мы применяем модуль |
Без наличия условий failed_when
и
changed_when
, нам бы пришлось создать двухступенчатую комбинированную
задачу чтобы уберечься от ошибок:
- name: check if branch badfeature exists
command: git branch
args:
chdir: /srv/app
register: branches
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
when: branches.stdout | search('badfeature')
В том случае, когда данное подразделение не существует, исполнение данных задач выглядит следующим образом:
Хотя данный набор задач и работает, он не эффективен. Давайте улучшим его и усилимся имеющейся функциональность
failed_when
для уменьшения двух задач до одной:
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
register: gitout
failed_when:
- gitout.rc != 0
- not gitout.stderr | search('branch.*not found')
Замечание | |
---|---|
Множество условий, которые обычно соединяются неким |
Мы проверим код возврата данной команды на что- либо отличное от 0
и затем применим фильтр поиска чтобы отыскать необходимое значение stderr
с каким- то regex branch.*not found
. Мы воспользовались логикой
Jinja2 для соединения двух данных условий, которые будут оцениваться на вхождение неких вариантов
true
или false
:
Аналогично определению некоторого отказа задачи, также имеется и возможность определения что представляет
собой некоторое результат изменённой задачи. Эта возможность в особенности полезна с семейством модулей
command
(command
,
shell
, raw
и
script
). В отличии от большинства прочих модулей, все модули
этого семейства не имеют встроенных данных или некоторой неотъемлемого представления о том каким может быть
изменение. На самом деле, пока не определено иное, уди модули могут иметь результатом
только failed
,
changed
или
skipped
. Просто не существует никакого
способа для этих модулей допустить некое изменённое условие взамен не изменённого.
Условная зависимость changed_when
делает возможным для некоторого автора
плейбука указывать модулю как интерпретировать какое- то изменение. В точности как и для
failed_when
, changed_when
выполняет некую проверку для выработки какого- то Булева результата. Зачастую те задачи, которые используются с
changed_when
являются командами, которые завершатся с ненулевым результатом
для указания того, что никакой работы не должно выполняться, поэтому часто авторы будут соединять
changed_when
и failed_when
для тонкой настройки оценки результата такой задачи. В нашем предыдущем примере определённое условие
failed_when
отлавливало тот случай, при котором не должно было исполняться
никаких действий, однако сама задача всё же показывала некоторое изменение. Мы хотим зарегистрировать некоторое
изменение при коде завершения 0
, но ни для каких других кодов возврата.
Давайте расширим свой пример для выполнения этого:
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
register: gitout
failed_when:
- gitout.rc != 0
- not gitout.stderr | search('branch.*not found')
changed_when: gitout.rc == 0
Теперь, если мы исполним свою задачу когда данное подразделение всё ещё не существует, мы увидим следующий вывод:
Отметим, что теперь наш ключ changed
имеет своим значением
false
.
Просто для завершённости, давайте изменим свой сценарий таким образом, чтобы это подразделение имелось в
наличии и выполним его вновь. Для создания своего подразделения просто исполните
git branch badfeature
в своём каталоге
/srv/app
. Теперь мы можем исполнить наш плейбук вновь для просмотра
вывода который будет таким:
На этот раз наш вывод отличается; имеется зарегистрированное изменение, причём полученные в
stdout
данные отображают то подразделение, которое подлежит удалению.
Некое подмножество имеющегося семейства модулей command
(command
, shell
и
script
) имеет некую пару особых параметров, которые повлияют на то, была
ли уже выполнена определённая работа данной задачи, следовательно, будет ли некая задача приводить к какому- то
изменению. Именно такими аргументами и являются creates
и
removes
. Эти два аргумента ожидают в качестве значения некий путь файла.
Когда Ansible предпринимает попытку исполнить какую- то задачу с определёнными аргументами
creates
и removes
, он вначале
проверит существуют ли указанные пути к файлу. Если такой путь имеется и был применён аргумент
creates
, Ansible будет считать что данная работа уже выполнена и возвратит
Ok
. Наоборот, если данный путь не существует и применяется аргумент
removes
, результатом также будет Ok
.
Все прочие комбинации вызовут реальное выполнение работы. Ожидается, что любая работа которую выполнит данная задача,
будет иметь результатом либо создание, либо удаление того файла, на который имеется ссылка.
Само устройство creates
и removes
уберегает разработчиков от необходимости выполнять две задачи в одной. Давайте создадим некий вариант, при котором
мы желаем выполнить свой сценарий frobitz
из некоторого подкаталога нашего
корня проекта. Для нашего случая мы знаем, что наш сценарий frobitz
создаст некий путь /srv/whiskey/tango
. Фактически, исходный код
frobitz
таков:
#!/bin/bash
rm -rf /srv/whiskey/tango
mkdir /srv/whiskey/tango
Мы не хотим чтобы этот сценарий исполнялся дважды, поскольку это может разрушить все существующие данные. Комбинация двух задач будет выглядеть примерно так:
- name: discover tango directory
stat: path=/srv/whiskey/tango
register: tango
- name: run frobitz
script: files/frobitz --initialize /srv/whiskey/tango
when: not tango.stat.exists
Допустим, что этот файл уже имеется, тогда вывод будет таким:
Если же запрошенный путь /srv/whiskey/tango
не существует, наш модуль
stat возвратил бы намного меньше данных и результат ключа exists
имел
бы значение false
. Таким образом, был бы исполнен наш сценарий
frobitz
.
Теперь мы применим creates
для уменьшения всего до единственной
задачи:
- name: run frobitz
script: files/frobitz creates=/srv/whiskey/tango
Совет | |
---|---|
Наш модуль |
На это раз наш вывод будет слегка отличаться:
Совет | |
---|---|
Надлежащее применение |
Порой бывает желательно полностью подавить изменения. Это часто применяется когда исполняется некая
команда чтобы получать данные. Такое исполнение команды на самом деле ничего не изменяет; вместо этого она только
извлекает информацию, например, как модуль setup
. Подавление
изменений в таких задачах может быть полезным для быстрого определения того привело ли исполнение плейбука в
результате к некому реальному изменению на борту.
Для подавления изменений просто воспользуйтесь false
в качестве
некоторого аргумента для имеющегося ключа задачи changed_when
.
Давайте расширим один из наших предыдущих примеров по обнаружению активных сеансов
iscsi
чтобы подавить изменения:
- name: discover iscsi sessions
command: /sbin/iscsiadm -m session
register: sessions
failed_when:
- sessions.rc != 0
- not sessions.stderr |
search('No active sessions')
changed_when: false
Теперь, вне зависимости от возвращаемых результатов Ansible будет рассматривать данную задачу как
Ok
вместо внесения изменений:
Секция rescue
некоторого блока определяет какой- то логический элемент
задачи, который будет исполняться если внутри некоторого block
должен
быть обнаружен истинный сбой. Так как Ansible выполняет все задачи внутри некоторого
block
сверху вниз, когда истинный отказ встретится, исполнение перескочит
на самую первую задачу имеющегося раздела rescue
данного
block
, если она имеется. Затем задачи выполняются сверху вниз пока не будет
достигнут самый конец этого раздела или не произойдёт новая ошибка. После завершения такого раздела
rescue
, задача продолжит исполнение с тем, что поступает после данного
block
, как если бы не было никакой ошибки. Это предоставляет некий способ
тщательной обработки ошибок, позволяя определять задачи очистки с тем, чтобы система не оставалась в полностью
разрушенном состоянии и оставшееся воспроизведение могло бы продолжиться. Это намного яснее чем некий сложный
набор зарегистрированных задачей результатов и задач условных зависимостей на основании состояния ошибки.
Для демонстрации такого решения, давайте создадим некий новый набор задач внутри некоторого
block
. Этот набор задач будет иметь внутри некую не обрабатываемую ошибку,
которая вызовет переключение исполнения в имеющийся раздел rescue
,
в котором мы выполним некую задачу очистки. Мы также предоставим некую задачу после такого
block
, чтобы обеспечить продолжение исполнения. Мы повторно используем
свой плейбук errors.yaml
:
---
- name: error handling
hosts: localhost
gather_facts: false
tasks:
- block:
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
- name: this task is lost
debug:
msg: "I do not get seen"
rescue:
- name: cleanup task
debug:
msg: "I am cleaning up"
- name: cleanup task 2
debug:
msg: "I am also cleaning up"
- name: task after block
debug:
msg: "Execution goes on"
Когда это воспроизведение исполнится, первая задача получит результатом некую ошибку и наша вторая задача будет обойдена стороной. Исполнение продолжится с имеющейся задачи очистки, как мы это можем наблюдать в данном снимке экрана:
Но не только имеющийся раздел rescue
будет исполнен, также выполнится
и оставшаяся часть данного воспроизведения и всё исполнение ansible-playbook
было рассмотрено как успешное.
Помимо раздела rescue
доступен и другой раздел с названием
always
. Этот раздел некоторого
block
будет всегда исполняться были ли ошибки, или нет. Такая
функциональность удобна чтобы обеспечить текущее состояние некоторой системы всегда остающееся рабочим, будь
некий block
задач успешен или нет. Поскольку некоторые задачи какого- то
block
могут быть пропущены из- за ошибок, а некий раздел
rescue
исполняется только когда имеется какая- то ошибка, именно
раздел always
предоставляет обязательное гарантированное исполнение
задачи в любом случае.
Давайте расширим наш предыдущий пример и добавим некий раздел always
в наш block
:
always:
- name: most important task
debug:
msg: "Never going to let you down"
Повторно исполнив свой плейбук мы увидим отображённой свою дополнительную задачу:
Чтобы проверить что данный раздел always
на самом деле всегда
исполняется, мы можем поменять своё воспроизведение с тем чтобы наша задача git рассматривалась как успешная.
Для удобства здесь показан этот плейбук целиком:
---
- name: error handling
hosts: localhost
gather_facts: false
tasks:
- block:
- name: delete branch bad
command: git branch -D badfeature
args:
chdir: /srv/app
register: gitout
failed_when:
- gitout.rc != 0
- not gitout.stderr | search('branch.*not found')
- name: this task is lost
debug:
msg: "I do not get seen"
rescue:
- name: cleanup task
debug:
msg: "I am cleaning up"
- name: cleanup task 2
debug:
msg: "I am also cleaning up"
always:
- name: most important task
debug:
msg: "Never going to let you down"
- name: task after block
debug:
msg: "Execution goes on"
На этот раз, когда мы исполняем свой плейбук, наш раздел rescue
пропускается, наша маскированная в прошлый раз задача исполняется, а наш блок
always
всё равно при этом получает управление:
В целом Ansible отлично справляется с заданием при определении когда имеются отказы или действительные изменения выполняются некоторой задачей. Тем не менее, иногда, Ansible либо не способен, либо просто требует некоторой тонкой настройки на основе определённого случая. Чтобы справиться с этим, имеется некий набор построения задач для применения авторами плейбуков. Кроме того, блоки задач предоставляют некий способ аккуратного возврата из ошибок с тем, чтобы могла быть исполнена процедура очистки и могло быть завершено оставшееся воспроизведение (воспроизведения). В своей следующей главе мы изучим как применять Роли для организации задач, файловЮ, переменных и прочего содержимого.