поднимает systemd‑сервис node_exporter с прослушкой 0.0.0.0:9100;

включает textfile collector через --collector.textfile.directory=/var/lib/node_exporter/textfile_collector;

открывает порт 9100 в firewalld;

кладёт тестовый .prom для проверки, что textfile collector реально работает.

Переменные
node_exporter_user, node_exporter_group — системный пользователь и группа, под которыми будет работать сервис.

node_exporter_bin_path — путь к бинарнику node_exporter (ты кладёшь его в /usr/local/bin/node_exporter).

node_exporter_service_path — путь к unit‑файлу node_exporter.service.

node_exporter_listen_address — адрес для HTTP‑эндпойнта метрик (0.0.0.0:9100).

node_exporter_textfile_dir — каталог, откуда textfile collector будет читать все .prom‑файлы.

% cat node_exporter.yml 
---
- name: Установка Prometheus Node Exporter из локального архива
  hosts: all
  become: true

  vars:
    node_exporter_user: node_exporter
    node_exporter_group: node_exporter
    node_exporter_bin_path: /usr/local/bin/node_exporter
    node_exporter_service_path: /etc/systemd/system/node_exporter.service
    node_exporter_listen_address: "0.0.0.0:9100"
    node_exporter_textfile_dir: /var/lib/node_exporter/textfile_collector

  tasks:
    - name: Создаём системную группу для node_exporter
      ansible.builtin.group:
        name: "{{ node_exporter_group }}"
        system: true

    - name: Создаём системного пользователя для node_exporter
      ansible.builtin.user:
        name: "{{ node_exporter_user }}"
        group: "{{ node_exporter_group }}"
        shell: /sbin/nologin
        system: true
        create_home: false

    - name: Создаём каталог textfile collector
      ansible.builtin.file:
        path: "{{ node_exporter_textfile_dir }}"
        state: directory
        owner: "{{ node_exporter_user }}"
        group: "{{ node_exporter_group }}"
        mode: "0775"


    - name: Копируем бинарник node_exporter
      ansible.builtin.copy:
        src: file/node_exporter
        dest: "{{ node_exporter_bin_path }}"
        owner: root
        group: root
        mode: "0755"
      notify: Restart node_exporter

    - name: Создаём systemd unit для node_exporter
      ansible.builtin.copy:
        dest: "{{ node_exporter_service_path }}"
        owner: root
        group: root
        mode: "0644"
        content: |
          [Unit]
          Description=Prometheus Node Exporter
          Wants=network-online.target
          After=network-online.target

          [Service]
          User={{ node_exporter_user }}
          Group={{ node_exporter_group }}
          Type=simple
          ExecStart={{ node_exporter_bin_path }} --web.listen-address={{ node_exporter_listen_address }} --collector.textfile.directory={{ node_exporter_textfile_dir }}
          Restart=on-failure
          RestartSec=5s

          [Install]
          WantedBy=multi-user.target
      notify:
        - Reload systemd
        - Restart node_exporter

    - name: Включаем и запускаем node_exporter
      ansible.builtin.systemd:
        name: node_exporter
        enabled: true
        state: started

    - name: Проверяем, установлен ли firewalld
      ansible.builtin.service_facts:

    - name: Открываем порт 9100/tcp в firewalld
      ansible.posix.firewalld:
        port: 9100/tcp
        permanent: true
        immediate: true
        state: enabled
      when: "'firewalld.service' in ansible_facts.services"

    - name: Кладём тестовый .prom файл
      ansible.builtin.copy:
        dest: "{{ node_exporter_textfile_dir }}/test.prom"
        owner: "{{ node_exporter_user }}"
        group: "{{ node_exporter_group }}"
        mode: "0644"
        content: |
          # HELP ansible_test_metric Тестовая метрика из Ansible
          # TYPE ansible_test_metric gauge
          ansible_test_metric 1

    - name: Кладём skeleton bash script для textfile collector
      ansible.builtin.copy:
        dest: /usr/local/bin/skelet.sh
        owner: root
        group: root
        mode: "0700"
        content: |
          #!/usr/bin/env bash
          set -u

          TEXTFILE_COLLECTOR_DIR="{{ node_exporter_textfile_dir }}"
          PROM_BASENAME="skelet.prom"
          TMP_FILE="${TEXTFILE_COLLECTOR_DIR}/${PROM_BASENAME}.$$"
          FINAL_FILE="${TEXTFILE_COLLECTOR_DIR}/${PROM_BASENAME}"

          START_TS="$(date +%s)"

          # === ТУТ НУЖНА ТВОЯ ПОЛЕЗНАЯ КОМАНДА ===
          # пример:
          # /usr/local/bin/my-script.sh
          # rc=$?
          true
          rc=$?

          END_TS="$(date +%s)"
          DURATION="$((END_TS - START_TS))"

          cat > "${TMP_FILE}" <<EOF
          # HELP skelet_last_rc Exit code of the last command
          # TYPE skelet_last_rc gauge
          skelet_last_rc ${rc}

          # HELP skelet_duration_seconds Script execution time in seconds
          # TYPE skelet_duration_seconds gauge
          skelet_duration_seconds ${DURATION}

          # HELP skelet_last_run_unixtime Last script run timestamp
          # TYPE skelet_last_run_unixtime gauge
          skelet_last_run_unixtime ${END_TS}
          EOF

          mv "${TMP_FILE}" "${FINAL_FILE}"


  handlers:
    - name: Reload systemd
      ansible.builtin.systemd:
        daemon_reload: true

    - name: Restart node_exporter
      ansible.builtin.systemd:
        name: node_exporter
        state: restarted
        enabled: true

Вариант универсального скелета

    - name: Кладём универсальный skelet.sh (передаём ему что выполнить)
      ansible.builtin.copy:
        dest: /usr/local/bin/skelet.sh
        owner: root
        group: root
        mode: "0755"
        content: |
          #!/usr/bin/env bash
          #
          # Универсальный wrapper для Prometheus Node Exporter textfile collector.
          # Использование:
          #   skelet.sh -n <job_name> [-d <dir>] [-l <log_dir>] [-t <timeout_sec>] -- <команда> [аргументы]
          #
          # Пример:
          #   skelet.sh -n backup_db -t 300 -- /usr/local/bin/backup-db.sh --full
          #
          set -u
          set -o pipefail

          TEXTFILE_COLLECTOR_DIR="{{ node_exporter_textfile_dir }}"
          LOG_DIR="/var/log/skelet"
          LOCK_DIR="/var/run/skelet"
          TIMEOUT=""
          JOB_NAME=""

          usage() {
            cat <<USAGE
          Использование: $(basename "$0") -n <job_name> [-d <dir>] [-l <log_dir>] [-t <timeout_sec>] -- <команда> [аргументы]

            -n   Имя задачи (обязательно, используется как label job="...")
            -d   Каталог textfile collector (по умолчанию: ${TEXTFILE_COLLECTOR_DIR})
            -l   Каталог логов (по умолчанию: ${LOG_DIR})
            -t   Таймаут выполнения команды в секундах (без ограничения если не указан)
            --   Разделитель, дальше — сама команда с аргументами
          USAGE
            exit 1
          }

          while getopts ":n:d:l:t:h" opt; do
            case "${opt}" in
              n) JOB_NAME="${OPTARG}" ;;
              d) TEXTFILE_COLLECTOR_DIR="${OPTARG}" ;;
              l) LOG_DIR="${OPTARG}" ;;
              t) TIMEOUT="${OPTARG}" ;;
              h) usage ;;
              *) usage ;;
            esac
          done
          shift $((OPTIND - 1))
          [[ "${1:-}" == "--" ]] && shift

          if [[ -z "${JOB_NAME}" || $# -eq 0 ]]; then
            echo "Ошибка: не указано имя задачи (-n) или команда для выполнения." >&2
            usage
          fi

          SAFE_JOB_NAME="$(echo -n "${JOB_NAME}" | tr -c 'a-zA-Z0-9_' '_')"

          mkdir -p "${TEXTFILE_COLLECTOR_DIR}" "${LOG_DIR}" "${LOCK_DIR}"

          LOCK_FILE="${LOCK_DIR}/${SAFE_JOB_NAME}.lock"
          LOG_FILE="${LOG_DIR}/${SAFE_JOB_NAME}.log"
          PROM_FILE="${TEXTFILE_COLLECTOR_DIR}/skelet_${SAFE_JOB_NAME}.prom"
          TMP_FILE="${PROM_FILE}.$$.tmp"

          # Не даём одной и той же задаче запуститься параллельно
          exec 9>"${LOCK_FILE}"
          if ! flock -n 9; then
            echo "$(date '+%F %T') задача '${JOB_NAME}' уже выполняется, выходим." >> "${LOG_FILE}"
            exit 0
          fi

          START_TS="$(date +%s)"

          if [[ -n "${TIMEOUT}" ]]; then
            timeout --signal=TERM --kill-after=10 "${TIMEOUT}" "$@" >>"${LOG_FILE}" 2>&1
          else
            "$@" >>"${LOG_FILE}" 2>&1
          fi
          rc=$?

          END_TS="$(date +%s)"
          DURATION="$((END_TS - START_TS))"

          cat > "${TMP_FILE}" <<EOF
          # HELP skelet_last_run_exit_code Код возврата последнего запуска задачи
          # TYPE skelet_last_run_exit_code gauge
          skelet_last_run_exit_code{job="${JOB_NAME}"} ${rc}

          # HELP skelet_last_run_success Успешность последнего запуска (1 = успех, 0 = ошибка)
          # TYPE skelet_last_run_success gauge
          skelet_last_run_success{job="${JOB_NAME}"} $([[ ${rc} -eq 0 ]] && echo 1 || echo 0)

          # HELP skelet_duration_seconds Время выполнения задачи в секундах
          # TYPE skelet_duration_seconds gauge
          skelet_duration_seconds{job="${JOB_NAME}"} ${DURATION}

          # HELP skelet_last_run_timestamp_seconds Unix-время завершения последнего запуска
          # TYPE skelet_last_run_timestamp_seconds gauge
          skelet_last_run_timestamp_seconds{job="${JOB_NAME}"} ${END_TS}
          EOF

          mv -f "${TMP_FILE}" "${PROM_FILE}"
          exit "${rc}"