migration vers monitoring

This commit is contained in:
2026-03-16 13:42:53 +01:00
parent a999510c6c
commit 4e329c870f
13 changed files with 1585 additions and 258 deletions

View File

@@ -0,0 +1,354 @@
#!/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
Ligne brute:
$line
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

@@ -0,0 +1,48 @@
#!/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
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
WARNING=80
CRITICAL=95
MOUNTS=("/" "/var" "/home")
for mount in "${MOUNTS[@]}"; do
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

View File

@@ -0,0 +1,163 @@
#!/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 -euo pipefail
BASE_DIR="/opt/monitoring"
CONF_DIR="${BASE_DIR}/conf"
LOG_DIR="/var/log/monitoring"
STATE_DIR="/var/lib/monitoring"
LOCK_DIR="/var/lock/monitoring"
TMP_DIR="/tmp/monitoring-install"
UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux"
MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
INSTALL_DEPS="${INSTALL_DEPS:-true}"
CREATE_LOCAL_CONF="${CREATE_LOCAL_CONF:-true}"
require_root() {
if [ "${EUID}" -ne 0 ]; then
echo "Ce script doit être exécuté en root." >&2
exit 1
fi
}
install_deps() {
if [ "${INSTALL_DEPS}" != "true" ]; then
return 0
fi
if command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y curl coreutils findutils grep sed gawk util-linux ca-certificates
fi
}
prepare_dirs() {
mkdir -p "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}"
chmod 755 "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}"
}
fetch_manifest() {
curl -fsS "${MANIFEST_URL}" -o "${TMP_DIR}/manifest.txt"
}
validate_manifest() {
awk '
NF == 3 &&
$1 ~ /^[0-9a-fA-F]{64}$/ &&
$2 ~ /^(644|755|600)$/ &&
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
$3 !~ /\.\./
' "${TMP_DIR}/manifest.txt" >/dev/null
}
apply_mode() {
local mode="$1"
local file="$2"
chmod "$mode" "$file"
}
download_one() {
local expected_hash="$1"
local mode="$2"
local rel_path="$3"
local url="${UPDATE_BASE_URL}/${rel_path}"
local dst="${BASE_DIR}/${rel_path}"
local tmp_file
tmp_file="$(mktemp "${TMP_DIR}/file.XXXXXX")"
curl -fsS "$url" -o "$tmp_file"
local got_hash
got_hash="$(sha256sum "$tmp_file" | awk '{print $1}')"
if [ "$got_hash" != "$expected_hash" ]; then
echo "Hash invalide pour ${rel_path}" >&2
rm -f "$tmp_file"
return 1
fi
mkdir -p "$(dirname "$dst")"
apply_mode "$mode" "$tmp_file"
mv -f "$tmp_file" "$dst"
}
install_from_manifest() {
while read -r hash mode rel_path; do
[ -n "${hash:-}" ] || continue
download_one "$hash" "$mode" "$rel_path"
done < "${TMP_DIR}/manifest.txt"
}
create_local_conf_if_missing() {
if [ "${CREATE_LOCAL_CONF}" != "true" ]; then
return 0
fi
if [ ! -f "${CONF_DIR}/alert-engine.local.conf" ]; then
cat > "${CONF_DIR}/alert-engine.local.conf" <<'EOF'
#!/bin/bash
NTFY_SERVER="https://ntfy.sh"
NTFY_TOPIC="FjdJ7qex2oGqZkV3OMaqNIxe"
NTFY_TOKEN="A_REMPLACER"
DEST="root"
EOF
chmod 600 "${CONF_DIR}/alert-engine.local.conf"
fi
}
show_next_steps() {
cat <<'EOF'
Installation terminée.
Étapes suivantes :
1. Éditer /opt/monitoring/conf/alert-engine.local.conf
2. Remplacer NTFY_TOKEN par le vrai token
3. Tester :
/opt/monitoring/bin/check_disk.sh
/opt/monitoring/bin/alert-engine.sh
4. Ajouter cron ou systemd timer
Exemple cron :
*/5 * * * * /opt/monitoring/bin/check_disk.sh
*/5 * * * * /opt/monitoring/bin/check_ram.sh
15 */6 * * * /opt/monitoring/bin/check_cert.sh
30 2 * * * /opt/monitoring/bin/check_backup.sh
10 3 * * * /opt/monitoring/bin/monitoring-update.sh
* * * * * /opt/monitoring/bin/alert-engine.sh
EOF
}
main() {
require_root
install_deps
prepare_dirs
fetch_manifest
if ! validate_manifest; then
echo "Le manifeste est invalide." >&2
exit 1
fi
install_from_manifest
create_local_conf_if_missing
show_next_steps
}
main "$@"

View File

@@ -0,0 +1,214 @@
#!/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/autoupdate.conf"
load_conf_if_exists "/opt/monitoring/conf/autoupdate.local.conf"
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
case "$rel_path" in
conf/alert-engine.local.conf|conf/autoupdate.local.conf|conf/monitoring.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 le fichier local absent du manifeste" \
"file=$rel_path"
done
}
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
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

@@ -0,0 +1,206 @@
#!/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/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