menage et adaptation dans check_disk

This commit is contained in:
2026-03-17 07:59:05 +01:00
parent 35f3c6f5f7
commit 3b05390ec4
7 changed files with 29 additions and 35 deletions

View File

@@ -1,352 +0,0 @@
#!/bin/bash
# Copyright (C) 2026 Cédric Abonnel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
set -u
SCRIPT_NAME="$(basename "$0")"
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
load_conf_if_exists "/opt/monitoring/conf/alert-engine.conf"
load_conf_if_exists "/opt/monitoring/conf/alert-engine.local.conf"
lock_or_exit "alert-engine"
require_cmd awk sed grep date tail stat cut tr
LOG_SOURCE="${LOG_FILE:-/var/log/monitoring/events.jsonl}"
STATE_FILE="${ALERT_STATE_FILE:-/var/lib/monitoring/alert-engine.offset}"
DEDUP_FILE="${ALERT_DEDUP_FILE:-/var/lib/monitoring/alert-engine.dedup}"
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$DEDUP_FILE")" || fail_internal "Impossible de créer les répertoires d'état"
touch "$STATE_FILE" "$DEDUP_FILE" || fail_internal "Impossible d'initialiser les fichiers d'état"
json_get() {
local key="$1"
local line="$2"
printf '%s\n' "$line" \
| sed -n "s/.*\"${key}\":\"\([^\"]*\)\".*/\1/p" \
| head -n1
}
json_get_number() {
local key="$1"
local line="$2"
printf '%s\n' "$line" \
| sed -n "s/.*\"${key}\":\([0-9][0-9]*\).*/\1/p" \
| head -n1
}
get_last_offset() {
local offset
offset="$(cat "$STATE_FILE" 2>/dev/null || true)"
if [[ "$offset" =~ ^[0-9]+$ ]]; then
printf '%s\n' "$offset"
else
printf '0\n'
fi
}
set_last_offset() {
printf '%s\n' "$1" > "$STATE_FILE"
}
current_log_size() {
stat -c '%s' "$LOG_SOURCE" 2>/dev/null || printf '0\n'
}
cleanup_dedup_file() {
local now window tmp
now="$(date +%s)"
window="${ALERT_DEDUP_WINDOW:-3600}"
tmp="$(mktemp "${MONITORING_STATE_DIR}/alert-engine.dedup.XXXXXX")" || return 0
awk -F'|' -v now="$now" -v window="$window" '
NF >= 2 {
if ((now - $2) <= window) print $0
}
' "$DEDUP_FILE" > "$tmp" 2>/dev/null || true
mv -f "$tmp" "$DEDUP_FILE" 2>/dev/null || true
}
dedup_key() {
local host="$1"
local app="$2"
local level="$3"
local event="$4"
printf '%s|%s|%s|%s\n' "$host" "$app" "$level" "$event"
}
should_notify_dedup() {
local key="$1"
local now window found_ts
now="$(date +%s)"
window="${ALERT_DEDUP_WINDOW:-3600}"
found_ts="$(awk -F'|' -v k="$key" '
$1 "|" $3 "|" $4 "|" $5 == k {print $2}
' "$DEDUP_FILE" | tail -n1)"
if [[ "$found_ts" =~ ^[0-9]+$ ]]; then
if [ $((now - found_ts)) -lt "$window" ]; then
return 1
fi
fi
return 0
}
save_dedup_entry() {
local host="$1"
local app="$2"
local level="$3"
local event="$4"
local now
now="$(date +%s)"
printf '%s|%s|%s|%s|%s\n' "$host" "$now" "$app" "$level" "$event" >> "$DEDUP_FILE"
}
event_is_ignored() {
local event="$1" ignored
for ignored in ${ALERT_IGNORE_EVENTS:-}; do
[ "$ignored" = "$event" ] && return 0
done
return 1
}
channels_for_event() {
local level="$1"
local event="$2"
local varname value
varname="ALERT_RULE_${event}"
value="${!varname:-}"
if [ -n "$value" ]; then
printf '%s\n' "$value"
return 0
fi
case "$level" in
WARNING)
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_WARNING:-ntfy}"
;;
ERROR)
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_ERROR:-ntfy,mail}"
;;
CRITICAL)
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_CRITICAL:-ntfy,mail}"
;;
*)
printf '\n'
;;
esac
}
tags_for_level() {
case "$1" in
WARNING) printf '%s\n' "${NTFY_TAGS_WARNING:-warning}" ;;
ERROR) printf '%s\n' "${NTFY_TAGS_ERROR:-warning,rotating_light}" ;;
CRITICAL) printf '%s\n' "${NTFY_TAGS_CRITICAL:-skull,warning}" ;;
*) printf '\n' ;;
esac
}
send_ntfy() {
local title="$1"
local body="$2"
local priority="$3"
[ "${ALERT_NTFY_ENABLED:-true}" = "true" ] || return 0
[ -n "${NTFY_SERVER:-}" ] || return 1
[ -n "${NTFY_TOPIC:-}" ] || return 1
local url="${NTFY_SERVER%/}/${NTFY_TOPIC}"
local curl_args=(
-fsS
-X POST
-H "Title: ${title}"
-H "Priority: ${priority}"
-H "Tags: warning"
-d "$body"
)
# topic protégé
if [ -n "${NTFY_TOKEN:-}" ]; then
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
fi
curl "${curl_args[@]}" "$url" >/dev/null
}
send_mail() {
local subject="$1"
local body="$2"
[ "${ALERT_MAIL_ENABLED:-true}" = "true" ] || return 0
[ -n "${DEST:-}" ] || return 1
[ -x "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" ] || return 1
{
printf 'To: %s\n' "${DEST}"
printf 'Subject: %s %s\n' "${ALERT_MAIL_SUBJECT_PREFIX:-[monitoring]}" "$subject"
printf 'Content-Type: text/plain; charset=UTF-8\n'
printf '\n'
printf '%s\n' "$body"
} | "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" -t
}
priority_for_level() {
case "$1" in
CRITICAL) printf 'urgent\n' ;;
ERROR) printf 'high\n' ;;
WARNING) printf 'default\n' ;;
*) printf 'default\n' ;;
esac
}
build_title() {
local host="$1"
local app="$2"
local level="$3"
local event="$4"
printf '%s [%s] %s %s\n' "$host" "$app" "$level" "$event"
}
build_body() {
local ts="$1"
local host="$2"
local app="$3"
local level="$4"
local event="$5"
local message="$6"
local line="$7"
cat <<EOF
Date: $ts
Hôte: $host
Script: $app
Niveau: $level
Événement: $event
Message:
$message
EOF
}
process_line() {
local line="$1"
local ts host app level event message channels title body prio ch key
ts="$(json_get "ts" "$line")"
host="$(json_get "host" "$line")"
app="$(json_get "app" "$line")"
level="$(json_get "level" "$line")"
event="$(json_get "event" "$line")"
message="$(json_get "message" "$line")"
local tags
tags="$(tags_for_level "$level")"
[ -n "$level" ] || return 0
[ -n "$event" ] || return 0
case "$level" in
DEBUG|INFO|NOTICE)
return 0
;;
esac
if event_is_ignored "$event"; then
return 0
fi
key="$(dedup_key "$host" "$app" "$level" "$event")"
if ! should_notify_dedup "$key"; then
log_debug "alert_suppressed_dedup" "Alerte supprimée par déduplication" \
"event=$event" "level=$level" "host=$host" "app=$app"
return 0
fi
channels="$(channels_for_event "$level" "$event")"
[ -n "$channels" ] || return 0
title="$(build_title "$host" "$app" "$level" "$event")"
body="$(build_body "$ts" "$host" "$app" "$level" "$event" "$message" "$line")"
prio="$(priority_for_level "$level")"
IFS=',' read -r -a channel_array <<< "$channels"
for ch in "${channel_array[@]}"; do
case "$ch" in
ntfy)
if send_ntfy "$title" "$body" "$prio" "$tags"; then
log_info "alert_sent_ntfy" "Notification ntfy envoyée" \
"event=$event" "level=$level" "host=$host" "app=$app"
else
log_error "alert_ntfy_failed" "Échec d'envoi ntfy" \
"event=$event" "level=$level" "host=$host" "app=$app"
fi
;;
mail)
if send_mail "$title" "$body"; then
log_info "alert_sent_mail" "Mail d'alerte envoyé" \
"event=$event" "level=$level" "host=$host" "app=$app"
else
log_error "alert_mail_failed" "Échec d'envoi mail" \
"event=$event" "level=$level" "host=$host" "app=$app"
fi
;;
esac
done
save_dedup_entry "$host" "$app" "$level" "$event"
}
main() {
local last_offset log_size
last_offset="$(get_last_offset)"
log_size="$(current_log_size)"
if [ ! -f "$LOG_SOURCE" ]; then
log_notice "alert_log_missing" "Fichier de log absent, rien à traiter" "file=$LOG_SOURCE"
exit 0
fi
if [ "$last_offset" -gt "$log_size" ]; then
log_notice "alert_offset_reset" "Offset réinitialisé après rotation ou troncature du log" \
"old_offset=$last_offset" "new_offset=0"
last_offset=0
fi
cleanup_dedup_file
tail -c +$((last_offset + 1)) "$LOG_SOURCE" | while IFS= read -r line; do
[ -n "$line" ] || continue
process_line "$line"
done
set_last_offset "$log_size"
}
main
exit_with_status

View File

@@ -19,35 +19,34 @@ WARNING=80
CRITICAL=95
MOUNTS=("/" "/var" "/home")
for mount in "${MOUNTS[@]}"; do
# On vérifie si le point de montage existe avant de tester
if ! mountpoint -q "$mount"; then
log_to_php() {
local level="$1"
local event="$2"
local message="$3"
shift 3
# On passe les arguments restants au format JSON ou chaine à PHP
php -r "
require '/opt/monitoring/lib/monitoring-lib.php';
log_event('$level', '$event', '$message', ["$*"]);
"
}
for mount in "${MOUNTS[@]}"; do
if ! mountpoint -q "$mount"; then continue; fi
used_pct="$(df -P "$mount" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')"
if [[ ! "$used_pct" =~ ^[0-9]+$ ]]; then
log_to_php "ERROR" "check_failed" "Impossible de lire l'usage" "'mount=$mount'"
continue
fi
used_pct="$(df -P "$mount" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')"
if [[ ! "$used_pct" =~ ^[0-9]+$ ]]; then
log_error "check_failed" "Impossible de lire l'utilisation disque" "mount=$mount"
continue
fi
level="$(threshold_level "$used_pct" "$WARNING" "$CRITICAL")"
case "$level" in
INFO)
log_info "disk_ok" "Utilisation disque normale" \
"mount=$mount" "used_pct=$used_pct" "warning=$WARNING" "critical=$CRITICAL"
;;
WARNING)
log_warning "disk_usage_high" "Utilisation disque élevée" \
"mount=$mount" "used_pct=$used_pct" "warning=$WARNING" "critical=$CRITICAL"
;;
CRITICAL)
log_critical "disk_usage_critical" "Utilisation disque critique" \
"mount=$mount" "used_pct=$used_pct" "warning=$WARNING" "critical=$CRITICAL"
;;
esac
done
exit_with_status
# Détermination du niveau (logique bash simple)
if [ "$used_pct" -ge "$CRITICAL" ]; then
log_to_php "CRITICAL" "disk_usage_critical" "Disque plein" "'mount=$mount', 'used=$used_pct%'"
elif [ "$used_pct" -ge "$WARNING" ]; then
log_to_php "WARNING" "disk_usage_high" "Disque presque plein" "'mount=$mount', 'used=$used_pct%'"
else
log_to_php "INFO" "disk_ok" "Disque OK" "'mount=$mount', 'used=$used_pct%'"
fi
done

View File

@@ -1,112 +0,0 @@
#!/bin/bash
# Copyright (C) 2026 Cédric Abonnel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
set -u
SCRIPT_NAME="$(basename "$0")"
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
# On s'assure d'avoir les permissions root
if [ "${EUID}" -ne 0 ]; then
echo "Ce script doit être exécuté en root." >&2
exit 1
fi
extract_keys() {
local file="$1"
grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$file" | cut -d'=' -f1 | sort -u
}
check_config_drift() {
local conf_dir="/opt/monitoring/conf"
local base_conf local_conf
local found_issue=false
local reviewed_files=0
local files_requiring_action=0
log_info "audit_start" "Début de l'audit des configurations locales"
while IFS= read -r base_conf; do
reviewed_files=$((reviewed_files + 1))
local_conf="${base_conf%.conf}.local.conf"
local file_name local_file_name
file_name="$(basename "$base_conf")"
local_file_name="$(basename "$local_conf")"
if [ ! -f "$local_conf" ]; then
cp "$base_conf" "$local_conf" || {
log_error "audit_create_local_failed" \
"Impossible de créer ${local_file_name} à partir de ${file_name}"
found_issue=true
files_requiring_action=$((files_requiring_action + 1))
continue
}
chmod 600 "$local_conf" 2>/dev/null || true
log_notice "audit_missing_local" \
"Le fichier ${local_file_name} n'existait pas ; il a été créé par copie de ${file_name}"
continue
fi
local tmp_base tmp_local
tmp_base="$(mktemp)" || fail_internal "mktemp a échoué"
tmp_local="$(mktemp)" || fail_internal "mktemp a échoué"
extract_keys "$base_conf" > "$tmp_base"
extract_keys "$local_conf" > "$tmp_local"
local missing obsolete
missing="$(comm -23 "$tmp_base" "$tmp_local" | xargs)"
obsolete="$(comm -13 "$tmp_base" "$tmp_local" | xargs)"
if [ -n "$missing" ] || [ -n "$obsolete" ]; then
found_issue=true
files_requiring_action=$((files_requiring_action + 1))
log_warning "audit_file_requires_action" \
"Le fichier ${local_file_name} nécessite une vérification"
if [ -n "$missing" ]; then
log_warning "audit_keys_missing" \
"Dans ${local_file_name}, options disponibles dans ${file_name} mais absentes du local : ${missing}"
fi
if [ -n "$obsolete" ]; then
log_info "audit_keys_obsolete" \
"Dans ${local_file_name}, options présentes uniquement dans le local et à vérifier ou supprimer : ${obsolete}"
fi
else
log_info "audit_file_ok" \
"Le fichier ${local_file_name} contient les mêmes options que ${file_name}"
fi
rm -f "$tmp_base" "$tmp_local"
done < <(find "$conf_dir" -maxdepth 1 -type f -name "*.conf" ! -name "*.local.conf" | sort)
if [ "$found_issue" = false ]; then
log_info "audit_success" \
"Toutes les configurations locales sont à jour (${reviewed_files} fichier(s) vérifié(s))"
else
log_warning "audit_requires_action" \
"Certaines configurations locales doivent être mises à jour (${files_requiring_action} fichier(s) à vérifier sur ${reviewed_files})"
fi
}
main() {
lock_or_exit "monitoring-audit"
check_config_drift
}
main
exit_with_status

View File

@@ -1,247 +0,0 @@
#!/bin/bash
# Copyright (C) 2026 Cédric Abonnel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# Moteur de mise à jour des programmes et fichiers connexes
set -u
SCRIPT_NAME="$(basename "$0")"
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
load_conf_if_exists "/opt/monitoring/conf/autoupdate.conf"
load_conf_if_exists "/opt/monitoring/conf/autoupdate.local.conf"
# Définir les variables par défaut si elles ne sont pas dans les fichiers .conf
UPDATE_TMP_DIR="${UPDATE_TMP_DIR:-/tmp/monitoring-update}"
UPDATE_TIMEOUT_CONNECT="${UPDATE_TIMEOUT_CONNECT:-3}"
UPDATE_TIMEOUT_TOTAL="${UPDATE_TIMEOUT_TOTAL:-15}"
UPDATE_MANIFEST_URL="${UPDATE_MANIFEST_URL:-}"
UPDATE_BASE_URL="${UPDATE_BASE_URL:-}"
lock_or_exit "monitoring-update"
require_cmd curl sha256sum awk mktemp chmod dirname mv rm grep sed sort comm cut tr find
[ "${UPDATE_ENABLED:-true}" = "true" ] || {
log_notice "update_disabled" "Mise à jour désactivée par configuration"
exit 0
}
mkdir -p "${UPDATE_TMP_DIR:-/tmp/monitoring-update}" || fail_internal "Impossible de créer le répertoire temporaire"
TMP_MANIFEST="$(mktemp "${UPDATE_TMP_DIR}/manifest.XXXXXX")" || fail_internal "mktemp a échoué"
TMP_LOCAL_LIST="$(mktemp "${UPDATE_TMP_DIR}/local.XXXXXX")" || fail_internal "mktemp a échoué"
TMP_REMOTE_LIST="$(mktemp "${UPDATE_TMP_DIR}/remote.XXXXXX")" || fail_internal "mktemp a échoué"
cleanup() {
rm -f "$TMP_MANIFEST" "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST"
}
trap cleanup EXIT
fetch_manifest() {
if ! curl -fsS \
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
"${UPDATE_MANIFEST_URL}" \
-o "$TMP_MANIFEST"; then
log_error "manifest_download_failed" \
"Impossible de télécharger le manifeste" \
"url=${UPDATE_MANIFEST_URL}"
return 1
fi
if ! awk '
NF == 3 &&
$1 ~ /^[0-9a-fA-F]{64}$/ &&
$2 ~ /^(644|755)$/ &&
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
$3 !~ /\.\./
' "$TMP_MANIFEST" >/dev/null; then
log_error "manifest_invalid" \
"Le manifeste distant est invalide" \
"url=${UPDATE_MANIFEST_URL}"
return 1
fi
log_info "manifest_downloaded" "Manifeste téléchargé" "url=${UPDATE_MANIFEST_URL}"
return 0
}
list_remote_files() {
awk '{print $3}' "$TMP_MANIFEST" | sort -u > "$TMP_REMOTE_LIST"
}
list_local_files() {
find "${MONITORING_BASE_DIR}/bin" "${MONITORING_BASE_DIR}/lib" "${MONITORING_BASE_DIR}/conf" \
-type f 2>/dev/null \
| sed "s#^${MONITORING_BASE_DIR}/##" \
| sort -u > "$TMP_LOCAL_LIST"
}
apply_mode() {
local mode="$1"
local file="$2"
case "$mode" in
755) chmod 755 "$file" ;;
644) chmod 644 "$file" ;;
*) fail_internal "Mode non supporté: $mode" ;;
esac
}
update_one_file() {
local expected_hash="$1"
local mode="$2"
local rel_path="$3"
local local_file="${MONITORING_BASE_DIR}/${rel_path}"
local remote_file="${UPDATE_BASE_URL}/${rel_path}"
local tmp_file local_hash downloaded_hash
tmp_file="$(mktemp "${UPDATE_TMP_DIR}/$(basename "$rel_path").XXXXXX")" || fail_internal "mktemp a échoué"
if [ -f "$local_file" ]; then
local_hash="$(sha256sum "$local_file" | awk '{print $1}')"
else
local_hash=""
fi
if [ "$local_hash" = "$expected_hash" ]; then
log_debug "update_not_needed" "Fichier déjà à jour" "file=$rel_path"
rm -f "$tmp_file"
return 0
fi
if ! curl -fsS \
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
"$remote_file" \
-o "$tmp_file"; then
log_error "update_download_failed" \
"Téléchargement impossible" \
"file=$rel_path" "url=$remote_file"
rm -f "$tmp_file"
return 1
fi
downloaded_hash="$(sha256sum "$tmp_file" | awk '{print $1}')"
if [ "$downloaded_hash" != "$expected_hash" ]; then
log_error "update_hash_mismatch" \
"Hash téléchargé invalide" \
"file=$rel_path" "expected=$expected_hash" "got=$downloaded_hash"
rm -f "$tmp_file"
return 1
fi
ensure_parent_dir "$local_file"
apply_mode "$mode" "$tmp_file"
safe_mv "$tmp_file" "$local_file"
if [ -z "$local_hash" ]; then
log_notice "file_created" \
"Fichier créé depuis le manifeste" \
"file=$rel_path" "mode=$mode" "hash=$expected_hash"
else
log_notice "update_applied" \
"Mise à jour appliquée" \
"file=$rel_path" "mode=$mode" "old_hash=$local_hash" "new_hash=$expected_hash"
fi
return 0
}
delete_extra_local_files() {
[ "${UPDATE_ALLOW_DELETE:-false}" = "true" ] || return 0
comm -23 "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST" | while IFS= read -r rel_path; do
[ -n "$rel_path" ] || continue
# Protection globale de TOUS les fichiers .local.conf
if [[ "$rel_path" == *.local.conf ]]; then
log_notice "delete_skipped" \
"Fichier local protégé (ignoré)" \
"file=$rel_path"
continue
fi
# Sécurité supplémentaire pour ne pas supprimer les répertoires vitaux
rm -f "${MONITORING_BASE_DIR}/${rel_path}" \
&& log_notice "file_deleted" \
"Fichier obsolète supprimé" \
"file=$rel_path" \
|| log_error "delete_failed" \
"Échec suppression" \
"file=$rel_path"
done
}
run_local_conf_sync() {
local sync_script="${MONITORING_BASE_DIR}/bin/sync-local-confs.sh"
if [ -x "$sync_script" ]; then
log_info "local_conf_sync_start" \
"Synchronisation des fichiers .local.conf"
if "$sync_script"; then
log_info "local_conf_sync_done" \
"Synchronisation terminée"
else
log_warning "local_conf_sync_failed" \
"La synchronisation des .local.conf a échoué"
fi
else
log_notice "local_conf_sync_missing" \
"Script de synchronisation absent" \
"script=$sync_script"
fi
}
main() {
local total=0 updated_or_checked=0 failed=0
local hash mode path
fetch_manifest || exit 2
list_remote_files
list_local_files
while read -r hash mode path; do
[ -n "${hash:-}" ] || continue
total=$((total + 1))
if update_one_file "$hash" "$mode" "$path"; then
updated_or_checked=$((updated_or_checked + 1))
else
failed=$((failed + 1))
fi
done < "$TMP_MANIFEST"
delete_extra_local_files
run_local_conf_sync
if [ "$failed" -gt 0 ]; then
log_warning "update_finished_with_errors" \
"Mise à jour terminée avec erreurs" \
"total=$total" "updated_or_checked=$updated_or_checked" "failed=$failed"
else
log_info "update_finished" \
"Mise à jour terminée" \
"total=$total" "updated_or_checked=$updated_or_checked" "failed=0"
fi
}
main
exit_with_status

View File

@@ -1,208 +0,0 @@
#!/bin/bash
# Copyright (C) 2026 Cédric Abonnel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# lit le fichier log
set -u
SCRIPT_NAME="$(basename "$0")"
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
load_conf_if_exists "/opt/monitoring/conf/autoupdate.conf"
lock_or_exit "monitoring-update"
require_cmd curl sha256sum awk mktemp chmod dirname mv rm grep sed sort comm cut tr
[ "${UPDATE_ENABLED:-true}" = "true" ] || {
log_notice "update_disabled" "Mise à jour désactivée par configuration"
exit 0
}
mkdir -p "${UPDATE_TMP_DIR:-/tmp/monitoring-update}" || fail_internal "Impossible de créer le répertoire temporaire"
TMP_MANIFEST="$(mktemp "${UPDATE_TMP_DIR}/manifest.XXXXXX")" || fail_internal "mktemp a échoué"
TMP_LOCAL_LIST="$(mktemp "${UPDATE_TMP_DIR}/local.XXXXXX")" || fail_internal "mktemp a échoué"
TMP_REMOTE_LIST="$(mktemp "${UPDATE_TMP_DIR}/remote.XXXXXX")" || fail_internal "mktemp a échoué"
cleanup() {
rm -f "$TMP_MANIFEST" "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST"
}
trap cleanup EXIT
fetch_manifest() {
if ! curl -fsS \
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
"${UPDATE_MANIFEST_URL}" \
-o "$TMP_MANIFEST"; then
log_error "manifest_download_failed" "Impossible de télécharger le manifeste" "url=${UPDATE_MANIFEST_URL}"
return 1
fi
if ! awk '
NF == 3 &&
$1 ~ /^[0-9a-fA-F]{64}$/ &&
$2 ~ /^(644|755)$/ &&
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
$3 !~ /\.\./
' "$TMP_MANIFEST" >/dev/null; then
log_error "manifest_invalid" "Le manifeste distant est invalide" "url=${UPDATE_MANIFEST_URL}"
return 1
fi
log_info "manifest_downloaded" "Manifeste téléchargé" "url=${UPDATE_MANIFEST_URL}"
return 0
}
list_remote_files() {
awk '{print $3}' "$TMP_MANIFEST" | sort -u > "$TMP_REMOTE_LIST"
}
list_local_files() {
find "${MONITORING_BASE_DIR}/bin" "${MONITORING_BASE_DIR}/lib" "${MONITORING_BASE_DIR}/conf" \
-type f 2>/dev/null \
| sed "s#^${MONITORING_BASE_DIR}/##" \
| sort -u > "$TMP_LOCAL_LIST"
}
apply_mode() {
local mode="$1"
local file="$2"
case "$mode" in
755) chmod 755 "$file" ;;
644) chmod 644 "$file" ;;
*) fail_internal "Mode non supporté: $mode" ;;
esac
}
update_one_file() {
local expected_hash="$1"
local mode="$2"
local rel_path="$3"
local local_file="${MONITORING_BASE_DIR}/${rel_path}"
local remote_file="${UPDATE_BASE_URL}/${rel_path}"
local tmp_file local_hash downloaded_hash
tmp_file="$(mktemp "${UPDATE_TMP_DIR}/$(basename "$rel_path").XXXXXX")" || fail_internal "mktemp a échoué"
if [ -f "$local_file" ]; then
local_hash="$(sha256sum "$local_file" | awk '{print $1}')"
else
local_hash=""
fi
if [ "$local_hash" = "$expected_hash" ]; then
log_debug "update_not_needed" "Fichier déjà à jour" "file=$rel_path"
rm -f "$tmp_file"
return 0
fi
if ! curl -fsS \
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
"$remote_file" \
-o "$tmp_file"; then
log_error "update_download_failed" "Téléchargement impossible" "file=$rel_path" "url=$remote_file"
rm -f "$tmp_file"
return 1
fi
downloaded_hash="$(sha256sum "$tmp_file" | awk '{print $1}')"
if [ "$downloaded_hash" != "$expected_hash" ]; then
log_error "update_hash_mismatch" "Hash téléchargé invalide" \
"file=$rel_path" "expected=$expected_hash" "got=$downloaded_hash"
rm -f "$tmp_file"
return 1
fi
ensure_parent_dir "$local_file"
apply_mode "$mode" "$tmp_file"
safe_mv "$tmp_file" "$local_file"
if [ -z "$local_hash" ]; then
log_notice "file_created" "Fichier créé depuis le manifeste" \
"file=$rel_path" "mode=$mode" "hash=$expected_hash"
else
log_notice "update_applied" "Mise à jour appliquée" \
"file=$rel_path" "mode=$mode" "old_hash=$local_hash" "new_hash=$expected_hash"
fi
return 0
}
delete_extra_local_files() {
[ "${UPDATE_ALLOW_DELETE:-false}" = "true" ] || return 0
comm -23 "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST" | while IFS= read -r rel_path; do
[ -n "$rel_path" ] || continue
case "$rel_path" in
conf/autoupdate.conf|conf/alert-engine.local.conf)
log_notice "delete_skipped" \
"Suppression ignorée pour fichier local protégé" \
"file=$rel_path"
continue
;;
esac
rm -f "${MONITORING_BASE_DIR}/${rel_path}" \
&& log_notice "file_deleted" \
"Fichier supprimé car absent du manifeste" \
"file=$rel_path" \
|| log_error "delete_failed" \
"Impossible de supprimer fichier local" \
"file=$rel_path"
done
}
main() {
local total=0 updated=0 failed=0 hash mode path
fetch_manifest || exit 2
list_remote_files
list_local_files
while read -r hash mode path; do
[ -n "${hash:-}" ] || continue
total=$((total + 1))
if update_one_file "$hash" "$mode" "$path"; then
updated=$((updated + 1))
else
failed=$((failed + 1))
fi
done < "$TMP_MANIFEST"
delete_extra_local_files
if [ "$failed" -gt 0 ]; then
log_warning "update_finished_with_errors" \
"Mise à jour terminée avec erreurs" \
"total=$total" "updated_or_checked=$updated" "failed=$failed"
else
log_info "update_finished" \
"Mise à jour terminée" \
"total=$total" "updated_or_checked=$updated" "failed=0"
fi
}
main
exit_with_status