Прервать дальнейшее выполнение задачи с несколькими хостами

#ansible

#ansible

Вопрос:

Использование Ansible версии v2.9.12

Вопрос: Я бы хотел, чтобы Ansible завершал работу / останавливал воспроизведение при сбое задачи, когда несколько хостов выполняют задачу. В этом смысле Ansible должен прервать дальнейшее выполнение задачи. Конфигурация должна работать в роли, поэтому использование serial или использование разных игр невозможно.

Пример ;

 - hosts:
    - host1
    - host2
    - host3
  any_errors_fatal: true
  tasks:
    - name: always fail
      shell: /bin/false
      throttle: 1
  

Обеспечивает ;

 ===== task | always fail =======
host1: fail
host2: fail
host3: fail
  

Это означает, что задача все еще выполняется на втором хосте и третьем хосте. Я бы хотел, чтобы вся игра завершилась неудачно / остановилась, как только задача завершится неудачно на хосте. При сбое задачи на последнем хосте Ansible также должен прерваться.

Желаемый результат ;

 ===== task | always fail =======
host1: fail
host2: not executed/skipped, cause host1 failed
host3: not executed/skipped, cause host1 failed
  

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


Справочная информация: я разрабатывал идемпотентную роль Ansible для mysql. Можно настроить кластер с несколькими хостами. Роль также поддерживает добавление арбитра.

У арбитра не установлено приложение mysql, но хост по-прежнему требуется для воспроизведения.

Теперь представьте три хоста. Host1 является арбитром, у host2 и host3 установлен mysql, настроенный в кластере. Приложения настраиваются ролью Ansible. Теперь Ansible выполняет роль во второй / третий / четвертый / любой другой раз и изменяет настройку конфигурации mysql. Mysql требуется непрерывный перезапуск. Обычно кто-то пишет что-то вроде:

 - template:
    src: mysql.j2
    dest: /etc/mysql
  register: mysql_config
  when: mysql.role != 'arbiter'

- service:
    name: mysql
    state: restarted
  throttle: 1
  when:
    - mysql_config.changed
    - mysql.role != 'arbiter'
  

Недостатком этой конфигурации Ansible является то, что если mysql не запускается на host2 по какой-либо причине, Ansible также перезапустит mysql на host3. И это нежелательно, потому что если mysql также выходит из строя на host3, то кластер теряется. Итак, для этой конкретной задачи я бы хотел, чтобы Ansible остановил / прервал / пропустил другие задачи, если mysql не удалось запустить на одном хосте в игре.

Ответ №1:

Хорошо, это работает:

 # note that test-multi-01 set host_which_is_skipped: true
---
- hosts:
    - test-multi-01
    - test-multi-02
    - test-multi-03
  tasks:
    - set_fact:
        host_which_is_skipped: "{{ inventory_hostname }}"
      when: host_which_is_skipped

    - shell: /bin/false
      run_once: yes
      delegate_to: "{{ item }}"
      loop: "{{ ansible_play_hosts }}"
      when:
        - item != host_which_is_skipped
        - result is undefined or result is not failed
      register: result

    - meta: end_play
      when: result is failed

    - debug:
        msg: Will not happen
  

Когда для команды оболочки установлено значение /bin/true , команда выполняется на хостах 2 и host3.

Ответ №2:

Одним из способов решить эту проблему было бы запустить playbook с serial: 1 . Таким образом, задачи выполняются последовательно на хостах, и как только одна задача завершается неудачей, playbook завершается:

 - name: My playbook
  hosts: all
  serial: 1
  any_errors_fatal: true
  tasks:
  - name: Always fail
    shell: /bin/false
  

В этом случае это приводит к тому, что задача выполняется только на первом хосте. Обратите внимание, что существует также order предложение, с помощью которого вы также можете управлять порядком запуска хостов: https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html#hosts-and-users

Комментарии:

1. Для меня это невозможно. Конфигурация должна быть учтена в роли. И опять же, использование serial довольно схематично. И заказ воспроизведения бесполезен…

Ответ №3:

Отказ от ответственности: большая часть полностью рабочей части этого ответа относится к ответу @ Tomasz Klosinski на ошибку сервера.


Вот частично работающая идея, которая не дотягивает только до одного хоста.
Для демонстрации я намеренно увеличил количество своих хостов до 5 хостов.

Идея основана на специальных переменных ansible_play_batch и ansible_play_hosts_all , которые описаны на вышеупомянутой странице документа как:

  • ansible_play_hosts_all
    Список всех хостов, на которые была нацелена игра

  • ansible_play_batch Список активных хостов в текущем прогоне воспроизведения ограничен серийным номером, он же ‘пакет’. Сбойные / недоступные хосты не считаются ‘активными’.

Идея в сочетании с вашей пробной версией использования throttle: 1 должна сработать, но потерпит неудачу на одном хосте, выполняясь на host2 , когда он должен ее пропустить.

Учитывая сценарий:

 - hosts: all
  gather_facts: no
      
  tasks:
    - shell: /bin/false
      when: "ansible_play_batch | length == ansible_play_hosts_all | length"
      throttle: 1
  

Это приводит к повторению:

 PLAY [all] ***********************************************************************************************************

TASK [shell] *********************************************************************************************************
fatal: [host1]: FAILED! => {"changed": true, "cmd": "/bin/false", "delta": "0:00:00.003915", "end": "2020-09-06 22:09:16.550406", "msg": "non-zero return code", "rc": 1, "start": "2020-09-06 22:09:16.546491", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [host2]: FAILED! => {"changed": true, "cmd": "/bin/false", "delta": "0:00:00.004736", "end": "2020-09-06 22:09:16.844296", "msg": "non-zero return code", "rc": 1, "start": "2020-09-06 22:09:16.839560", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
skipping: [host3]
skipping: [host4]
skipping: [host5]

PLAY RECAP ***********************************************************************************************************
host1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
host2                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
host3                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
host4                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
host5                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
  

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

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

Итак, вот план действий:

 - hosts: all
  gather_facts: no
      
  tasks:
    - shell: /bin/false
      loop: "{{ ansible_play_hosts }}"
      register: failing_task
      when: "failing_task | default({}) is not failed"
      delegate_to: "{{ item }}"
      run_once: true
  

Это привело бы к повторению:

 PLAY [all] ***********************************************************************************************************

TASK [shell] *********************************************************************************************************
failed: [host1 -> host1] (item=host1) => {"ansible_loop_var": "item", "changed": true, "cmd": "/bin/false", "delta": "0:00:00.003706", "end": "2020-09-06 22:18:23.822608", "item": "host1", "msg": "non-zero return code", "rc": 1, "start": "2020-09-06 22:18:23.818902", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
skipping: [host1] => (item=host2) 
skipping: [host1] => (item=host3) 
skipping: [host1] => (item=host4) 
skipping: [host1] => (item=host5) 

NO MORE HOSTS LEFT ***************************************************************************************************

PLAY RECAP ***********************************************************************************************************
host1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
  

И просто ради доказательства того, что это работает по назначению, изменив его, чтобы сделать host2 сбой конкретно, с помощью failed_when :

 - hosts: all
  gather_facts: no
      
  tasks:
    - shell: /bin/false
      loop: "{{ ansible_play_hosts }}"
      register: failing_task
      when: "failing_task | default({}) is not failed"
      delegate_to: "{{ item }}"
      run_once: true
      failed_when: "item == 'host2'"
  

Выдает резюме:

 PLAY [all] ***********************************************************************************************************

TASK [shell] *********************************************************************************************************
changed: [host1 -> host1] => (item=host1)
failed: [host1 -> host2] (item=host2) => {"ansible_loop_var": "item", "changed": true, "cmd": "/bin/false", "delta": "0:00:00.004226", "end": "2020-09-06 22:20:38.038546", "failed_when_result": true, "item": "host2", "msg": "non-zero return code", "rc": 1, "start": "2020-09-06 22:20:38.034320", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
skipping: [host1] => (item=host3) 
skipping: [host1] => (item=host4) 
skipping: [host1] => (item=host5) 

NO MORE HOSTS LEFT ***************************************************************************************************

PLAY RECAP ***********************************************************************************************************
host1                      : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
  

Комментарии:

1. При использовании есть большой недостаток run_once . Если для этой задачи помещено другое предложение when, и первый хост, выполняющий эту задачу, должен пропустить эту задачу, сама задача пропускается. Пример: pastebin.com/knAknpU6 . Не могли бы вы придумать что-нибудь, что заставило бы это работать?

2. Mhm здесь это связано не с run_once , а с тем фактом, что вы используете when: ... and ... , который не ограничен конкретным хостом. Возможно, предоставление большего контекста, чем теоретический shell: /bin/false , приведет к тому, что кто-то даст вам ответ, которого вы, возможно, не ожидаете, но который будет работать в вашем случае использования.

3. Хорошо, я обновил свой вопрос некоторой справочной информацией. Я надеюсь, что это прояснит ситуацию.