# Отображение результата выполнения команды или скрипта К сожалению, в реальном времени отображать выполнение скрипта в Ansible не получается. Результат все же можно сохранить в переменную и далее в файл. При сохранении в файл на экране тоже отобразится. Пока использую `with_items`, как это делать с `loop`, надо отдельно тестировать, просто так заменить на `loop` нельзя. #### Выполнить команду на удаленном сервере и сохранить результат там или локально ```yml - 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 # без этого параметра сохранит в файл на сервере, где выполняется команда поиска ``` #### Выполнить на локальном сервере и сохранить результат тут же ```yml - 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`. ```yml - 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 }}" ``` # Использование поиска файлов find совместно с loop ```yml 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')`. ```yml - 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`](https://docs.ansible.com/ansible/latest/modules/find_module.html#parameter-paths) модуля find может принимать список путей, достаточно их добавить в переменную, например `work_dir`. Этот способ для моего первого примера не подходит, т.к. у нас нет готового списка каталогов— он формируется при использовании дополнительной переменной `home`. Подошло бы, если бы был готовый список `work_dir`: ```yml 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 обычно используется для перезапуска каких-то сервисов при изменении файлов конфигурации. handler выполняются в самом конце плейбука. Плюсы: если несколько событий notify вызвали один и тот же handler, он выполнится только один раз (не будет нескольких перезагрузок). Про минусы handler надо читать отдельно: например если какая-то таска завершилась с ошибкой, при этом она не имеет отношения к handler, все равно он не будет выполнен, т.к. плейбук не выполнился до конца. Читать про flush handlers. ## Универсальный handler и loop Как сделать универсальный handler, если например есть задача, которая через loop меняет файл одного из сервисов (Ansible сверяет контрольные сумммы и изменяет только те файлы, которые отличаются от файлов на сервере). При этом мы хотим перегрузить только сервис, конфигурация которого изменилась, а отдельные handler для каждого делать не хотим. ```yml # одна из тасков. Копируем шаблоны. Добавляем 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 }}" ``` ## Несколько задач на один хендлер ### Способ 1 — общий listen handlers могут использовать директиву прослушивания “listen”. Пример не совсем жизненный, т.к. здесь можно было в handler использовать одну task с loop, где перечислены memcached и apache, но общая схема ясна — слушаем один и тот же notify. ```yml 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 с двумя тасками более жизненный. Здесь вторая таска зависит от первой: ```yml - 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 ``` ### Способ 2 — перечисление хендлеров handlers/main.yml ```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 ```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" ``` Выполнение: ```bash 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 ``` # Идемпотентное изменение пароля пользователя Linux Одно из правил, которых мы должны придерживаться при создании ролей или плейбуков Ansible — идемпотентность: если роль выполняется много раз, целевой результат не должен изменяться. Вот пример: если у вас есть публичные ключи ssh для пользователей, которые вы раскидываете в их каталоги .ssh, проблем нет — новый ключ будет добавлен только при изменении файла с публичным ключом. Но что же будет, если мы в роли или плейбуке пытаемся задать пароли пользователей через модуль user? Не забываем кстати, что пароли хотя бы должны быть зашифрованы с помощью vault либо, как вариант, их можно вводить интерактивно во время выполнения роли. В чем здесь подвох? Сам механизм добавления пароля в Linux работает таким образом, что в /etc/shadow, где хранятся хеши паролей, никогда не будет одинаковых значений хэша, даже если мы задаём разным пользователям один и тот же простой пароль. Это происходит за счет того, что кроме нашего пароля Linux добавляет так называемую «соль» в хэш-функцию, что позволяет избежать проблемы простых и одинаковых паролей (чтоб защититься от атак по словарю). ```bash # 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-*`. [Исходники](https://github.com/shadow-maint/shadow/blob/master/src/passwd.c). Новый пароль создается с помощью функции `pw_encrypt(plain,salt)`. «Соль» (salt) создаётся функцией `crypt_make_salt()` из исходных кодов `shadow-*/libmisc/salt.c`. Эта функция использует переменную окружения `ENCRYPT_METHOD`. Если её нет, проверяется другая переменная окружения `MD5_CRYPT_ENAB`. ```c const char *method; (…) { method = getdef_str ("ENCRYPT_METHOD"); if (NULL == method) { method = getdef_bool ("MD5_CRYPT_ENAB") ? "MD5" : "DES"; } ``` При стандартном алгоритме шифрования SHA512 функция `crypt_make_salt()` создаёт соль соединением символов '$','6','$ ' и псевдорандомным числом: ```c 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: ```yml - name: Change password for user {{ linux_user }} user: name: "{{ linux_user }}" password: "{{ user_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}" ``` Почитать https://www.redhat.com/sysadmin/hashing-checksums