поднимает 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}"