#!/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" # 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 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