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