Глава 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
 	   

Исполнение такой задачи в том виде, как она представлена, выдаст нам некую ошибку:

 

Рисунок 4.1



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


- name: broken website
  uri:
    url: http://notahost.nodomain
  ignore_errors: true
 	   

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

 

Рисунок 4.2



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

Определение условия ошибки

Задание условия ignore_errors является достаточно грубым молотом. Все вырабатываемые в этом применяемом данной задачей модуле ошибки будут проигнорированы. Далее, полученный вывод, на первый взгляд, всё ещё выглядит как некая ошибка и может смутить некоторого оператора, пытающегося выяснить истинную причину отказа. Более аккуратным инструментом является условие failed_when. Это условие работает как скальпель и позволяет некоторому автору плейбука быть очень точным в отношении того, что составляет некую ошибку для какой- то задачи. Это условие выполняет некую проверку Булева результата, во многом схоже с условием when. Если результаты этого условия в Булевом выражении истинны, тогда эта задача будет рассматриваться как отказавшая. В противном случае данная задача будет трактоваться как успешная.

Такое условие failed_when очень полезно когда применяется совместно с модулем command или shell и регистрирует свой результат данного исполнения. Многие исполняющиеся программы могут иметь подробные ненулевые коды выхода, которые обозначают различные моменты, однако, все данные модули рассматривают некий отличающийся от нуля код выхода как какой- то отказ. Давайте рассмотрим утилиту определённую iscsiadm. Эта утилита может применяться для многих целей, относящихся к iSCSI. Ради целей нашей демонстрации мы применим её для обнаружения любых активных сеансов iscsi:


- name: query sessions
  command: /sbin/iscsiadm -m session
  register: sessions
 	   

Если это исполнить в некоторой системе, в которой отсутствуют активные сеансы, мы увидим вывод подобный такому:

 

Рисунок 4.3



Мы можем просто применить своё условие 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 должен рассматриваться как некий отказ. Давайте рассмотрим свой новый вывод после такого изменения:

 

Рисунок 4.4



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

Модули command и shell применяют различные форматы для предоставления аргументов модуля. Команда сама по себе предоставляет некую свободную форму, в то время как аргументы модуля идут в некий хэш args.

Если мы запустим только эту команду, мы получим некую ошибку с кодом возврата 1 если данное подразделение не существует:

 

Рисунок 4.5



[Замечание]Замечание

Мы применяем модуль command для более простой демонстрации своих целей несмотря на наличие самого модуля git. При работе с репозиториями git следует вместо этого применять имеющийся модуль git.

Без наличия условий 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')
 	   

В том случае, когда данное подразделение не существует, исполнение данных задач выглядит следующим образом:

 

Рисунок 4.6



Хотя данный набор задач и работает, он не эффективен. Давайте улучшим его и усилимся имеющейся функциональность 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')
 	   
[Замечание]Замечание

Множество условий, которые обычно соединяются неким and могут вместо этого быть выражены неким списком элементов. Это может сделать плейбуки более простыми для чтения и логические проблемы более быстрыми в обнаружении.

Мы проверим код возврата данной команды на что- либо отличное от 0 и затем применим фильтр поиска чтобы отыскать необходимое значение stderr с каким- то regex branch.*not found. Мы воспользовались логикой Jinja2 для соединения двух данных условий, которые будут оцениваться на вхождение неких вариантов true или false:

 

Рисунок 4.7



Определение изменения

Аналогично определению некоторого отказа задачи, также имеется и возможность определения что представляет собой некоторое результат изменённой задачи. Эта возможность в особенности полезна с семейством модулей 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
 	   

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

 

Рисунок 4.8



Отметим, что теперь наш ключ changed имеет своим значением false.

Просто для завершённости, давайте изменим свой сценарий таким образом, чтобы это подразделение имелось в наличии и выполним его вновь. Для создания своего подразделения просто исполните git branch badfeature в своём каталоге /srv/app. Теперь мы можем исполнить наш плейбук вновь для просмотра вывода который будет таким:

 

Рисунок 4.9



На этот раз наш вывод отличается; имеется зарегистрированное изменение, причём полученные в 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
 	   

Допустим, что этот файл уже имеется, тогда вывод будет таким:

 

Рисунок 4.10



Если же запрошенный путь /srv/whiskey/tango не существует, наш модуль stat возвратил бы намного меньше данных и результат ключа exists имел бы значение false. Таким образом, был бы исполнен наш сценарий frobitz.

Теперь мы применим creates для уменьшения всего до единственной задачи:


- name: run frobitz
  script: files/frobitz creates=/srv/whiskey/tango
 	   
[Совет]Совет

Наш модуль script в действительности некий action_plugin, который будет обсуждаться в Главе 8, Расширение Ansible. Именно только сценарий action_plugin принимает аргументы в указанном формате key=value.

На это раз наш вывод будет слегка отличаться:

 

Рисунок 4.11



[Совет]Совет

Надлежащее применение creates и removes позволит вашим плейбукам быть краткими и эффективными.

Неотложное изменение

Порой бывает желательно полностью подавить изменения. Это часто применяется когда исполняется некая команда чтобы получать данные. Такое исполнение команды на самом деле ничего не изменяет; вместо этого она только извлекает информацию, например, как модуль 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 вместо внесения изменений:

 

Рисунок 4.12



Восстановление после ошибки

rescue

Секция 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"
 	   

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

 

Рисунок 4.13



Но не только имеющийся раздел rescue будет исполнен, также выполнится и оставшаяся часть данного воспроизведения и всё исполнение ansible-playbook было рассмотрено как успешное.

always

Помимо раздела rescue доступен и другой раздел с названием always. Этот раздел некоторого block будет всегда исполняться были ли ошибки, или нет. Такая функциональность удобна чтобы обеспечить текущее состояние некоторой системы всегда остающееся рабочим, будь некий block задач успешен или нет. Поскольку некоторые задачи какого- то block могут быть пропущены из- за ошибок, а некий раздел rescue исполняется только когда имеется какая- то ошибка, именно раздел always предоставляет обязательное гарантированное исполнение задачи в любом случае.

Давайте расширим наш предыдущий пример и добавим некий раздел always в наш block:


always:
  - name: most important task
    debug:
      msg: "Never going to let you down"
 	   

Повторно исполнив свой плейбук мы увидим отображённой свою дополнительную задачу:

 

Рисунок 4.14



Чтобы проверить что данный раздел 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 всё равно при этом получает управление:

 

Рисунок 4.15



Выводы

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