К сожалению, в реальном времени отображать выполнение скрипта в Ansible не получается. Результат все же можно сохранить в переменную и далее в файл. При сохранении в файл на экране тоже отобразится.
Пока использую with_items
, как это делать с loop
, надо отдельно тестировать, просто так заменить на loop
нельзя.
- name: Play to run find command and capture its output to a file
hosts: my-test-host
tasks:
- name: 'Run find command to fetch file rights {{inventory_hostname}}'
command: "find / -type f -name '*.yml'"
register: find_results
become: true
become_user: root
become_method: sudo
- name: Print to verify it works
debug:
msg: '{{find_results.stdout}}'
- name: Use copy module to create the file using output from the previous command.
copy:
dest: find_results.txt # сохранится в каталог, где выполняется плейбук. Можно абсолютный путь /tmp/find_results.txt
content: "{{ item }}"
with_items: "{{ find_results.stdout }}" # осторожно, просто на loop заменить нельзя
delegate_to: localhost # без этого параметра сохранит в файл на сервере, где выполняется команда поиска
- name: Play to run find command and capture its output to a file
hosts: 127.0.0.1
connection: local
tasks:
- name: 'Run find command to fetch file rights {{inventory_hostname}}'
command: "find / -type f -name '*.yml'"
register: find_results
become: true
become_user: root
become_method: sudo
- name: Print to verify it works
debug:
msg: '{{find_results.stdout}}'
- name: Use copy module to create the file using output from the previous command.
copy:
dest: find_results.txt # сохранится в каталог, где выполняется плейбук. Можно абсолютный путь /tmp/find_results.txt
content: "{{ item }}"
with_items: "{{ find_results.stdout }}"
delegate_to: localhost
Пример со скриптом. Выполняет на удаленном сервере, файл вывода скрипта сохраняет во временном каталоге, заданном через переменную temp_dir
. В этом же каталоге лежит и сам скрипт install_script
.
- name: Install | Run script {{ install_script }}
shell: "./{{ install_script }}"
args:
chdir: "{{ temp_dir }}"
register: install
- name: Copy install log to file {{ temp_dir }}/install.log
copy:
dest: "{{ temp_dir }}/install.log"
content: "{{ item }}"
with_items: "{{ install.stdout }}"
defaults/main.yml
---
home: /home/tradematic
services:
- api
- backtest
- dataserver
- other
- execution
tasks/logs.yml
---
# поиск в нескольких каталогах
- name: register all logs-*.tgz files
find:
path: "{{ home }}/{{ item }}"
recurse: no
patterns: logs-*.tgz
loop: "{{ services }}"
register: tgzlist
# должен быть установлен jmespath
# копировать в каталог logs, который находится не внутри playbooks, а на уровне playbooks
- name: Copy collected logs *.tgz to localhost
fetch:
src: "{{ item }}"
dest: ../logs/
flat: yes
loop: "{{ tgzlist | json_query('results[*].files') |flatten | map(attribute='path') }}"
Ещё пример. Аналогично предыдущему, но грязный вывод экрана, т.к. отображается намного больше чем когда оставили только map(attribute='path')
.
- name: Find file
find:
paths: "{{ item }}"
use_regex: yes
patterns:
- '.*\.\d+\.\d{4}-\d{2}-\d{2}@\d{2}:\d{2}:\d{2}~$'
age: 1d
recurse: yes
register: fileToDelete
loop: "{{ fileToFindInAllSubDirecotry }}"
- name: Delete file
file:
path: "{{ item.path }}"
state: absent
loop: "{{ fileToDelete | json_query('results[*].files') | flatten }}"
Этот способ самый удобный, т.к. параметр paths
модуля find может принимать список путей, достаточно их добавить в переменную, например work_dir
. Этот способ для моего первого примера не подходит, т.к. у нас нет готового списка каталогов— он формируется при использовании дополнительной переменной home
. Подошло бы, если бы был готовый список work_dir
:
work_dir:
- /home/tradematic/api
- /home/tradematic/backtest
- /home/tradematic/dataserver
- /home/tradematic/other
- /home/tradematic/execution
tasks/logs.yml
# поиск в нескольких каталогах
- name: Find file
find:
paths: "{{ work_dir }}"
use_regex: yes
patterns:
- '.*\.\d+\.\d{4}-\d{2}-\d{2}@\d{2}:\d{2}:\d{2}~$'
age: 1d
recurse: yes
register: fileToDelete
- name: Delete file
file:
path: "{{ item.path }}"
state: absent
loop: "{{ fileToDelete.files }}"
handler обычно используется для перезапуска каких-то сервисов при изменении файлов конфигурации. handler выполняются в самом конце плейбука. Плюсы: если несколько событий notify вызвали один и тот же handler, он выполнится только один раз (не будет нескольких перезагрузок). Про минусы handler надо читать отдельно: например если какая-то таска завершилась с ошибкой, при этом она не имеет отношения к handler, все равно он не будет выполнен, т.к. плейбук не выполнился до конца. Читать про flush handlers.
Как сделать универсальный handler, если например есть задача, которая через loop меняет файл одного из сервисов (Ansible сверяет контрольные сумммы и изменяет только те файлы, которые отличаются от файлов на сервере). При этом мы хотим перегрузить только сервис, конфигурация которого изменилась, а отдельные handler для каждого делать не хотим.
# одна из тасков. Копируем шаблоны. Добавляем register и сохраняем результат выполнения команды в переменную podman_services
- name: create podman-compose files for services
template:
src: "{{ item }}/{{ item }}.yml.j2"
dest: "~/{{ item }}/{{ item }}.yml"
loop: "{{ services }}"
register: podman_services
notify: restart podman service
# в handlers/main.yml используем loop, где выуживаем из переменной только список измененных сервисов
# Перезапуск от лица пользователя с его переменными окружения
- name: restart podman service
become_user: "{{ user_name }}"
become_flags: -iS
systemd:
name: "podman-compose@{{ item }}"
scope: user
state: restarted
loop: "{{ podman_services.results | selectattr('changed') | map(attribute='item') | list }}"
handlers могут использовать директиву прослушивания “listen”. Пример не совсем жизненный, т.к. здесь можно было в handler использовать одну task с loop, где перечислены memcached и apache, но общая схема ясна — слушаем один и тот же notify.
tasks:
- name: restart everything
command: echo "this task will restart the web services"
notify: "restart web services"
....
handlers:
- name: restart memcached
service:
name: memcached
state: restarted
listen: "restart web services"
- name: restart apache
service:
name: apache
state: restarted
listen: "restart web services"
Вот такой пример hadler с двумя тасками более жизненный. Здесь вторая таска зависит от первой:
- name: Check if restarted
shell: check_is_started.sh
register: result
listen: Restart processes
- name: Restart conditionally step 2
service:
name: service
state: restarted
when: result
listen: Restart processes
handlers/main.yml
# handlers file for zabbix_agent
- name: ensure selinux tools are installed
ansible.builtin.package:
name:
- checkpolicy
- policycoreutils-python
state: latest
- name: create selinux mod for zabbix_agent
ansible.builtin.command: checkmodule -M -m -o /etc/zabbix/my-zabbixagent.mod /etc/zabbix/my-zabbixagent.te
- name: create selinux pp for zabbix_agent
ansible.builtin.command: semodule_package -o /etc/zabbix/my-zabbixagent.pp -m /etc/zabbix/my-zabbixagent.mod
- name: load selinux pp for zabbix_agent
ansible.builtin.command: semodule -i /etc/zabbix/my-zabbixagent.pp
...
Перечисляем несколько хенлеров в tasks/main.yml
- name: place selinux type enforcement
ansible.builtin.copy:
src: my-zabbixagent.te
dest: /etc/zabbix/my-zabbixagent.te
mode: "0644"
notify:
- ensure selinux tools are installed
- create selinux mod for zabbix_agent
- create selinux pp for zabbix_agent
- load selinux pp for zabbix_agent
when:
- ansible_selinux.status is defined
- ansible_selinux.status == "enabled"
Выполнение:
TASK [robertdebock.zabbix_agent : place selinux type enforcement] ************************************************************
Friday 06 August 2021 00:43:24 +0300 (0:00:00.824) 0:00:13.035 *********
changed: [preprod-db1]
TASK [robertdebock.zabbix_agent : start and enable zabbix agent] *************************************************************
Friday 06 August 2021 00:43:24 +0300 (0:00:00.713) 0:00:13.749 *********
ok: [preprod-db1]
RUNNING HANDLER [robertdebock.zabbix_agent : ensure selinux tools are installed] *********************************************
Friday 06 August 2021 00:43:25 +0300 (0:00:00.600) 0:00:14.349 *********
changed: [preprod-db1]
RUNNING HANDLER [robertdebock.zabbix_agent : create selinux mod for zabbix_agent] ********************************************
Friday 06 August 2021 00:43:30 +0300 (0:00:05.444) 0:00:19.794 *********
changed: [preprod-db1]
RUNNING HANDLER [robertdebock.zabbix_agent : create selinux pp for zabbix_agent] *********************************************
Friday 06 August 2021 00:43:31 +0300 (0:00:00.446) 0:00:20.240 *********
changed: [preprod-db1]
RUNNING HANDLER [robertdebock.zabbix_agent : load selinux pp for zabbix_agent] ***********************************************
Friday 06 August 2021 00:43:31 +0300 (0:00:00.294) 0:00:20.535 *********
changed: [preprod-db1]
PLAY RECAP *******************************************************************************************************************
preprod-db1 : ok=16 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Одно из правил, которых мы должны придерживаться при создании ролей или плейбуков Ansible — идемпотентность: если роль выполняется много раз, целевой результат не должен изменяться.
Вот пример: если у вас есть публичные ключи ssh для пользователей, которые вы раскидываете в их каталоги .ssh, проблем нет — новый ключ будет добавлен только при изменении файла с публичным ключом. Но что же будет, если мы в роли или плейбуке пытаемся задать пароли пользователей через модуль user? Не забываем кстати, что пароли хотя бы должны быть зашифрованы с помощью vault либо, как вариант, их можно вводить интерактивно во время выполнения роли. В чем здесь подвох? Сам механизм добавления пароля в Linux работает таким образом, что в /etc/shadow, где хранятся хеши паролей, никогда не будет одинаковых значений хэша, даже если мы задаём разным пользователям один и тот же простой пароль. Это происходит за счет того, что кроме нашего пароля Linux добавляет так называемую «соль» в хэш-функцию, что позволяет избежать проблемы простых и одинаковых паролей (чтоб защититься от атак по словарю).
# useradd bob
# passwd bob
New password:
Retype new password:
passwd: password updated successfully
# grep bob /etc/shadow
bob:$6$R22DWjRz5wd1iCqM$zJ98FQY5ghKj2A2DuoUf/ZxkdKMyjKIF6oheDGt4XBUgx4d6nLHOWYzbC3NU2hhMWgo/rxDp0M5g6mheTUiTc1:19074:0:99999:7:::
$6$
— алгоритм, используемый для хэш-функции. Здесь SHA-512. Менять в /etc/login.defs в переменной ENCRYPT_METHOD.
$R22DWjRz5wd1iCqM$
— значение между двумя знаками доллара и есть «соль».
zJ98FQY5ghKj2A2DuoUf/ZxkdKMyjKIF6oheDGt4XBUgx4d6nLHOWYzbC3NU2hhMWgo/rxDp0M5g6mheTUiTc1
— часть до двоеточия — захэшированный пароль, в качестве входных аргументов функции хэша используется соль и заданный нами пароль.
19074
— дата последнего изменения пароля, считается от рождества, но не Христова, а Unix — c 1 января 1970 года.
0
— минимальное кол-во дней между сменами пароля. При нуле пароль менять не требуется (кроме собственного желания).
99999
— максимальное разрешенное число дней между сменами пароля. При всех девятках пароль никогда не просрочится.
7
— число дней для отображении предупреждения о смене пароля. По умолчанию предупреждают за неделю.
Остальные значения не заданы, их назначение есть в документации.
Функция для «соли» определена в файле и каждый раз возвращает рандомное (случайное) значение.
=====
Кому интересно копнуть глубже: команда создания пароля passwd
задана в исходниках src/passwd.c
(язык Си) в библиотеке shadow-*
. Исходники. Новый пароль создается с помощью функции pw_encrypt(plain,salt)
.
«Соль» (salt) создаётся функцией crypt_make_salt()
из исходных кодов shadow-*/libmisc/salt.c
. Эта функция использует переменную окружения ENCRYPT_METHOD
. Если её нет, проверяется другая переменная окружения MD5_CRYPT_ENAB
.
const char *method;
(…)
{
method = getdef_str ("ENCRYPT_METHOD");
if (NULL == method) {
method = getdef_bool ("MD5_CRYPT_ENAB") ? "MD5" : "DES";
}
При стандартном алгоритме шифрования SHA512 функция crypt_make_salt()
создаёт соль соединением символов '$','6','$ ' и псевдорандомным числом:
if (0 == strcmp (method, "SHA512")) {
MAGNUM(result, '6');
strcat(result, SHA_salt_rounds((int *)arg));
salt_len = SHA_salt_size();
Функция pw_encrypt для пароля passwd вызывает функцию crypt из библиотеки libc. Функция crypt выбирает метод шифрования в зависимости от префикса соли.
====
Генерируемое случайное значение «соли» — проблема для идемпотентности Ansible. Получается, что даже если мы не изменяем пароль пользователя, каждый раз при выполнении роли хэш будет пересоздаваться, т.к. будет использоваться новое сгенерированное Linux-ом значение «соли». И каждый раз у нас после выполнения модуля будет changed — свидетельство того, что файл /etc/shadow
снова поменялся. Как сделать идемпотентным изменение пароля, описано в документации Ansible:
- name: Change password for user {{ linux_user }}
user:
name: "{{ linux_user }}"
password: "{{ user_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}"