Вопрос или проблема
У меня есть следующий Ansible playbook:
- hosts: <host>
become: true
vars:
dest_dir: <dir>
roles:
- {role: '<role>', tags: '<tag>'}
Вызываемая <role>
состоит из одного файла задачи:
- name: Копировать и проверить файл.
block:
- name: Копировать
ansible.builtin.copy:
src: <source file>
dest: "{{dest_dir}}"
mode: 0666
owner: root
group: root
backup: true
register: copy_result
- name: Печать результата
ansible.builtin.debug:
var: copy_result
- name: Валидация
ansible.builtin.shell: <script> 1
when: copy_result is changed
rescue:
- name: Восстановить
ansible.builtin.copy:
remote_src: true
src: "{{copy_result.backup_file}}"
dest: "{{dest_dir}}"
when: copy_result.backup_file is defined
Вызываемый <script>
является заглушкой, просто для выхода с кодом ошибки, соответствующим переданному аргументу:
exit $1
(Этот код основан на ответе в FAQ Ansible на https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#the-validate-option-is-not-enough-for-my-needs-what-do-i-do.)
Если я запущу это, когда целевой файл уже существует, но его содержимое отличается от содержимого исходного файла, Ansible правильно скопирует исходный файл в целевой, и я ожидал, что блок ‘Валидация’ приведет к вызову блока ‘rescue’, который должен “откатить” копирование файла. Однако блок ‘rescue’ не вызывается; вместо этого playbook прекращает выполнение в блоке ‘Валидация’ с сообщением:
TASK [<task> : Валидация] ******************************************************
fatal: [<host>]: FAILED! => {"changed": true, "cmd": "<script> 1", "delta": "0:00:00.010525", "end": "2024-09-17 10:49:23.559180", "msg": "не нулевой код возврата", "rc": 1, "start": "2024-09-17 10:49:23.548655", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
Затем он входит в отладчик.
Я задумывался, не в этом ли параметре ‘when’ блока ‘rescue’ заключается проблема, но комментирование его привело к такому же поведению.
Может кто-то выявить, что я делаю неправильно?
Во-первых, обратите внимание, что модуль ansible.builtin.copy
имеет опцию validate
. Используя ее, вы можете переписать свой playbook следующим образом:
- hosts: localhost
gather_facts: false
vars:
dest_dir: /tmp
tasks:
- name: Копировать
ansible.builtin.copy:
src: testfile
dest: "{{dest_dir}}/testfile"
mode: 0666
owner: root
group: root
backup: true
validate: sh -c 'exit 1' %s
Вы получите фактически такое же поведение с значительно меньшим объемом кода.
Что касается вашего вопроса, я не могу воспроизвести ошибку, которую вы показали. Вы не предоставили ее в своем вопросе, поэтому я создал следующий минимальный, воспроизводимый пример:
- hosts: localhost
gather_facts: false
vars:
dest_dir: /tmp
tasks:
- block:
- name: Копировать
ansible.builtin.copy:
src: testfile
dest: "{{dest_dir}}"
mode: 0666
owner: root
group: root
backup: true
register: copy_result
- name: Печать результата
ansible.builtin.debug:
var: copy_result
- name: Валидация
ansible.builtin.shell: "exit 1"
when: copy_result is changed
rescue:
- name: Восстановить
ansible.builtin.copy:
remote_src: true
src: "{{copy_result.backup_file}}"
dest: "{{dest_dir}}"
when: copy_result.backup_file is defined
Запуск этого кода производит (я использую unixy
вывод для уменьшения объема):
- localhost on hosts: localhost -
Копировать...
localhost done
Печать результата...
localhost ok: {
"changed": false,
"copy_result": {
"backup_file": "/tmp/testfile.893626.2024-09-17@07:11:55~",
"changed": true,
"checksum": "22596363b3de40b06f981fb85d82312e8c0ed511",
"dest": "/tmp/testfile",
"diff": [],
"failed": false,
"gid": 0,
"group": "root",
"md5sum": "6f5902ac237024bdd0c176cb93063dc4",
"mode": "0666",
"owner": "root",
"secontext": "unconfined_u:object_r:user_tmp_t:s0",
"size": 12,
"src": "/home/lars/.ansible/tmp/ansible-tmp-1726571514.9080791-893587-119392786896945/.source",
"state": "file",
"uid": 0
}
}
Валидация...
localhost failed | msg: не нулевой код возврата
Восстановить...
localhost ok
- Резюме выполнения -
localhost : ok=3 changed=1 unreachable=0 failed=0 rescued=1 ignored=0
Мы можем четко видеть, что задача “Восстановить” выполняется. Однако обратите внимание, что задача “Восстановить” не делает того, что вам нужно. В приведенном выше примере нашей целевой файл является /tmp/testfile
, что означает, что резервная копия файла будет называться, например, /tmp/testfile.893626.2024-09-17@07:11:55~
. Это делает задачу “Восстановить” эквивалентной:
- name: Восстановить
ansible.builtin.copy:
remote_src: true
src: "/tmp/testfile.893626.2024-09-17@07:11:55~"
dest: "/tmp"
when: copy_result.backup_file is defined
И это не операция; вы копируете резервный файл на сам себя. Если вы хотите заменить целевой файл, вам нужно будет указать явное имя файла в параметре dest
вместо директории; это сделает playbook выглядеть следующим образом:
- hosts: localhost
gather_facts: false
vars:
dest_dir: /tmp
tasks:
- block:
- name: Копировать
ansible.builtin.copy:
src: testfile
dest: "{{dest_dir}}/testfile"
mode: 0666
owner: root
group: root
backup: true
register: copy_result
- name: Печать результата
ansible.builtin.debug:
var: copy_result
- name: Валидация
ansible.builtin.shell: "exit 1"
when: copy_result is changed
rescue:
- name: Восстановить
ansible.builtin.copy:
remote_src: true
src: "{{copy_result.backup_file}}"
dest: "{{dest_dir}}/testfile"
when: copy_result.backup_file is defined
Запуск этого успешно откатит файл, когда задача “Валидация” завершится неудачно.
Ответ или решение
Ваша проблема связана с тем, как механизм блоков и блоков-rescue работает в Ansible, когда вы выполняете задачи, которые могут завершаться с ошибкой. Давайте разберем вашу ситуацию и предложим правильное решение.
Проблема:
В вашем плейбуке блок rescue
не срабатывает, когда задача Validate
завершает выполнение с ошибкой. Вместо этого выполнение плейбука прерывается, и вы видите сообщение о сбое с кодом возврата 1. Это может произойти из-за того, что задача Validate
завершает выполнение не совсем так, как ожидается, и Ansible не понимает, что это ошибка, требующая обработки через rescue
.
Решение:
Для того чтобы блок rescue
сработал, нужно убедиться, что ошибка действительно обрабатывается. Один из подходов — создать задачу в блоке, которая вызывает проверку с помощью модуля ansible.builtin.shell
, и обязательно указывать, что это может закончиться с ошибкой.
Вот как можно переписать ваш плейбук с учетом всех замечаний:
- hosts: <host>
become: true
vars:
dest_dir: <dir>
roles:
- { role: '<role>', tags: '<tag>' }
# В роли создаем задачу
tasks:
- block:
- name: Копирование файла
ansible.builtin.copy:
src: <source file>
dest: "{{ dest_dir }}"
mode: '0666'
owner: root
group: root
backup: true
register: copy_result
- name: Печать результата
ansible.builtin.debug:
var: copy_result
- name: Валидация
ansible.builtin.shell: <script> 1
register: validation_result
failed_when: validation_result.rc != 0 # Обрабатываем ошибки
rescue:
- name: Откат изменений
ansible.builtin.copy:
remote_src: true
src: "{{ copy_result.backup_file }}"
dest: "{{ dest_dir }}"
when: copy_result.backup_file is defined
Объяснение изменений:
-
failed_when
: В задачеValidate
я добавил параметрfailed_when
, который явным образом указывает, что задача считается неуспешной, когда код возврата (rc
) отличается от 0. Это позволяет Ansible корректно обрабатывать ошибку и не прерывать выполнение плейбука. -
Названия задач: Я обновил названия задач на русском, чтобы они были более понятными и отражали суть выполнения.
- Логика отката: Теперь
rescue
блок будет корректно выполняться, если задачаValidate
завершилась с ошибкой, и откат произойдет, как ожидается.
Дополнительные рекомендации:
- Убедитесь, что все пути к файлам и скриптам указаны правильно, чтобы избежать ошибок в процессе выполнения.
- Если предусмотрена возможность, что задачи могут завершаться с ошибкой, рассмотрите возможность использования модуля
ansible.builtin.command
вместоshell
для повышения безопасности, если нет необходимости в использовании оболочки.
С вышеуказанными исправлениями ваш плейбук должен корректно работать и выполнять задачу отката в случае, если валидация не проходит.