Skip to content

Instantly share code, notes, and snippets.

@anutator
Last active January 24, 2023 21:31
Show Gist options
  • Save anutator/8817abb8a9f4c03d6834928af4a9fb41 to your computer and use it in GitHub Desktop.
Save anutator/8817abb8a9f4c03d6834928af4a9fb41 to your computer and use it in GitHub Desktop.
Ansible Tips

Отображение результата выполнения команды или скрипта

К сожалению, в реальном времени отображать выполнение скрипта в 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 }}"

Использование поиска файлов find совместно с loop

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 обычно используется для перезапуска каких-то сервисов при изменении файлов конфигурации. handler выполняются в самом конце плейбука. Плюсы: если несколько событий notify вызвали один и тот же handler, он выполнится только один раз (не будет нескольких перезагрузок). Про минусы handler надо читать отдельно: например если какая-то таска завершилась с ошибкой, при этом она не имеет отношения к handler, все равно он не будет выполнен, т.к. плейбук не выполнился до конца. Читать про flush handlers.

Универсальный handler и loop

Как сделать универсальный 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 }}"

Несколько задач на один хендлер

Способ 1 — общий listen

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

Способ 2 — перечисление хендлеров

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  

Идемпотентное изменение пароля пользователя Linux

Одно из правил, которых мы должны придерживаться при создании ролей или плейбуков 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) }}"

Почитать https://www.redhat.com/sysadmin/hashing-checksums

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment