Compare commits

...

29 Commits

Author SHA1 Message Date
e6263c1ae2 historique des changements ajouté 2026-03-20 09:13:37 +01:00
4e7119297c journalisation des fichiers déployés 2026-03-20 08:49:42 +01:00
5e3b5ee741 suppression des fantomes sur les anciennes installation 2026-03-20 08:47:18 +01:00
95a6bb10b1 oups 2026-03-20 07:35:51 +01:00
42626f2204 install_deps : smartmontools 2026-03-20 07:34:32 +01:00
ac6b80cb69 simplificaiton config 2026-03-18 08:51:50 +01:00
88b0dd4e77 warning intelligent sur conf local 2026-03-18 08:37:39 +01:00
365c8e543f oups 2026-03-18 08:32:58 +01:00
61ebf5a92f crontab n'est pas modifié 2026-03-18 08:32:49 +01:00
12a7495447 rappel de configuration 2026-03-18 08:28:31 +01:00
7de4c86feb mise à jour update 2026-03-18 08:27:05 +01:00
d48cb923eb erreur sur wget 2026-03-18 08:22:29 +01:00
2540a96a1e oups 2026-03-18 08:20:22 +01:00
fba9bc89e2 mise à jour de la logique d'install et update 2026-03-18 08:20:13 +01:00
312ba59343 execution 2026-03-18 08:09:44 +01:00
78e2f5ea1e sécurité root 2026-03-18 08:07:45 +01:00
790052dfe5 oups 2026-03-18 08:06:35 +01:00
8074151300 ajout du script check_smart 2026-03-18 08:06:25 +01:00
b2c083eb8d oups 2026-03-17 08:19:29 +01:00
8b52a02b55 amélioration logs pour check disk 2026-03-17 08:18:03 +01:00
0aebf47f6b correction ntfy send 2026-03-17 08:14:39 +01:00
6541cefea0 journalisation en erreur 2026-03-17 08:08:02 +01:00
3b05390ec4 menage et adaptation dans check_disk 2026-03-17 07:59:05 +01:00
35f3c6f5f7 manifest 2026-03-17 07:52:27 +01:00
763bc0ba48 modification des channels 2026-03-17 07:46:24 +01:00
c9d6fd48ed bug dans update 2026-03-17 07:22:15 +01:00
3fd1c70bd7 execution des php 2026-03-17 07:16:38 +01:00
c280e4d5ac maj manifest 2026-03-17 07:15:56 +01:00
adfe0ed282 correction de README 2026-03-16 22:16:39 +01:00
20 changed files with 593 additions and 502 deletions

View File

@@ -1,20 +1,20 @@
# Lightweight Bash Monitoring System by Cédrix # 🛡️ PHP Monitoring System by Cédrix
Ce projet est une solution de monitoring légère, modulaire et auto-hébergée pour serveurs Linux. Elle permet de surveiller l'état des ressources (disque, RAM, etc.), de centraliser les logs au format JSON et d'envoyer des alertes via **ntfy** ou **email** avec un système de déduplication intelligent. Ce projet est une solution de monitoring **légère**, **modulaire** et **auto-hébergée** pour serveurs Linux. Elle combine la simplicité de sondes Bash avec la puissance d'un moteur de traitement PHP pour centraliser les logs et envoyer des alertes intelligentes via **ntfy** ou **email**.
## 🚀 Ce que fait ce système ## 🚀 Fonctionnalités clés
* **Sondes modulaires :** Scripts indépendants pour vérifier les ressources (ex: `check_disk.sh`). * **Moteur PHP & Sondes Hybrides :** Traitement performant des alertes en PHP, tout en gardant des sondes système simples (Bash ou PHP).
* **Logs JSONL :** Centralisation de tous les événements dans `/var/log/monitoring/events.jsonl` pour une analyse facile. * **Alertes Intelligentes :** Envoi via **ntfy** (avec tags et priorités) ou **email**, incluant un système de **déduplication** pour éviter le spam.
* **Moteur d'alerte :** Un moteur (`alert-engine.sh`) qui lit les logs en continu, gère les seuils de priorité et évite le spam grâce à une fenêtre de déduplication. * **Logs JSONL :** Centralisation au format standard `JSON Lines` dans `/var/log/monitoring/events.jsonl` pour une exploitation facile.
* **Auto-update :** Capacité de se mettre à jour automatiquement depuis un dépôt Git via un manifeste. * **Configuration en cascade :** Système de fichiers `.local.conf.php` pour protéger vos réglages lors des mises à jour.
* **Robuste :** Gestion des verrous (locks) pour éviter que deux instances d'un même script ne tournent en même temps. * **Auto-update & Audit :** Mise à jour automatique via manifeste et script d'audit pour détecter les nouvelles options de configuration manquantes.
--- ---
## 📦 Installation ## 📦 Installation
L'installation se fait via le script d'installation distant qui configure l'arborescence `/opt/monitoring`. L'installation se fait via un script Bash qui configure l'environnement et installe les dépendances nécessaires (PHP, curl).
```bash ```bash
# Passer en root # Passer en root
@@ -25,70 +25,65 @@ curl -sSL https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/serv
``` ```
### Arborescence créée : ### Structure du système :
* `/opt/monitoring/bin/` : Scripts exécutables (sondes, moteur, updateur). * `/opt/monitoring/bin/` : Exécutables (sondes, moteur `alert-engine.php`, updater).
* `/opt/monitoring/lib/` : Bibliothèque commune (`monitoring-lib.sh`). * `/opt/monitoring/lib/` : Bibliothèque partagée (`monitoring-lib.php`).
* `/opt/monitoring/conf/` : Fichiers de configuration. * `/opt/monitoring/conf/` : Fichiers de configuration PHP.
* `/var/log/monitoring/` : Logs des événements. * `/var/log/monitoring/` : Journal des événements (`events.jsonl`).
* `/var/lib/monitoring/` : États (offsets de lecture, déduplication). * `/var/lib/monitoring/` : Index (offsets de lecture, états de déduplication).
--- ---
## Configuration (Que modifier ?) ## ⚙️ Configuration
Après l'installation, vous devez configurer vos accès pour recevoir les alertes. Le système utilise des fichiers PHP pour la configuration afin de permettre une logique dynamique.
### 1. Alertes (ntfy / Mail) ### 1. Alertes (ntfy / Mail)
Éditez le fichier local (prioritaire sur la config par défaut) : Ne modifiez pas les fichiers `.conf.php` (risques d'écrasement). Créez vos fichiers locaux :
`nano /opt/monitoring/conf/alert-engine.local.conf` `cp /opt/monitoring/conf/alert-engine.conf.php /opt/monitoring/conf/alert-engine.local.conf.php`
Modifiez les variables suivantes : Éditez le fichier local pour renseigner :
* `NTFY_TOKEN` : Votre jeton d'accès ntfy. * `NTFY_TOKEN` & `NTFY_TOPIC`.
* `NTFY_TOPIC` : Le nom de votre topic. * `DEST` (votre email de réception).
* `DEST` : L'adresse email de réception.
### 2. Seuils des sondes ### 2. Audit de configuration
Vous pouvez modifier les variables `WARNING` et `CRITICAL` directement dans les scripts du dossier `bin/` ou, mieux, les définir dans `/opt/monitoring/conf/monitoring.local.conf`. Après une mise à jour, lancez l'outil d'audit pour vérifier si de nouvelles options sont disponibles :
--- ```bash
php /opt/monitoring/bin/monitoring-update-config.php
## Programmation (Quand exécuter ?)
Le système repose sur `cron`. Voici la configuration recommandée à ajouter via `crontab -e` :
| Tâche | Fréquence | Commande |
| --- | --- | --- |
| **Check Disque** | Toutes les 5 min | `/opt/monitoring/bin/check_disk.sh` |
| **Moteur d'alerte** | Toutes les 2 min | `/opt/monitoring/bin/alert-engine.sh` |
| **Mise à jour** | Une fois par jour | `/opt/monitoring/bin/monitoring-update.sh` |
### Exemple de Crontab :
```cron
*/5 * * * * /opt/monitoring/bin/check_disk.sh > /dev/null 2>&1
*/2 * * * * /opt/monitoring/bin/alert-engine.sh > /dev/null 2>&1
0 4 * * * /opt/monitoring/bin/monitoring-update.sh > /dev/null 2>&1
``` ```
## Développer une nouvelle sonde ---
Pour créer un nouveau check (ex: `check_ram.sh`) : ## 🕒 Planification (Crontab)
1. Inclure la lib : `. /opt/monitoring/lib/monitoring-lib.sh` Le système est conçu pour être piloté par `cron`. Voici la configuration recommandée :
2. Effectuer votre mesure.
3. Utiliser les fonctions de log : `log_info`, `log_warning` ou `log_critical`.
4. Le moteur d'alerte détectera automatiquement le nouvel événement au prochain passage.
| Tâche | Fréquence | Commande |
| --- | --- | --- |
| **Sondes (ex: Disque)** | Toutes les 5 min | `php /opt/monitoring/bin/check_disk.php` |
| **Moteur d'alerte** | Chaque minute | `php /opt/monitoring/bin/alert-engine.php` |
| **Mise à jour** | 1x par jour | `php /opt/monitoring/bin/monitoring-update.php` |
--- ---
## Licence ## 🛠️ Développer une nouvelle sonde
Ce projet est un logiciel libre : vous pouvez le redistribuer et le modifier selon les termes de la GNU Affero General Public License (AGPLv3) telle que publiée par la Free Software Foundation. Le système est agnostique. Pour ajouter un check :
Le code source modifié doit être mis à disposition si vous utilisez ce logiciel via un réseau (usage SaaS). 1. Créez un script qui écrit une ligne JSON dans `/var/log/monitoring/events.jsonl`.
2. Format attendu : `{"time":"...", "level":"ERROR", "app":"my_app", "event":"disk_full", "msg":"..."}`.
3. Le moteur PHP `alert-engine.php` traitera l'alerte automatiquement au prochain passage.
---
## ⚖️ Licence
Ce projet est distribué sous licence **GNU Affero General Public License (AGPLv3)**.
*Note : Si vous modifiez ce code pour l'utiliser via un réseau (SaaS), vous devez rendre vos modifications publiques.*

87
servers/linux/monitoring/bin/alert-engine.php Normal file → Executable file
View File

@@ -9,7 +9,7 @@
require_once __DIR__ . '/../lib/monitoring-lib.php'; require_once __DIR__ . '/../lib/monitoring-lib.php';
// --- Initialisation de la configuration spécifique --- // --- Initialisation de la configuration spécifique ---
// On charge les fichiers de conf s'ils existent (format PHP attendu) // On charge les fichiers de conf s'ils existent
foreach (["/opt/monitoring/conf/alert-engine.conf.php", "/opt/monitoring/conf/alert-engine.conf.local.php"] as $conf) { foreach (["/opt/monitoring/conf/alert-engine.conf.php", "/opt/monitoring/conf/alert-engine.conf.local.php"] as $conf) {
if (file_exists($conf)) { if (file_exists($conf)) {
$extra_conf = include $conf; $extra_conf = include $conf;
@@ -30,50 +30,97 @@ ensure_parent_dir($STATE_FILE);
ensure_parent_dir($DEDUP_FILE); ensure_parent_dir($DEDUP_FILE);
/** /**
* Nettoyage du fichier de déduplication (entrées expirées) * Nettoyage du fichier de déduplication
* Supprime les entrées plus vieilles que la fenêtre de déduplication ($DEDUP_WINDOW)
*/ */
function cleanup_dedup_file() { function cleanup_dedup_file() {
global $DEDUP_FILE, $DEDUP_WINDOW; global $DEDUP_FILE, $DEDUP_WINDOW;
if (!file_exists($DEDUP_FILE)) return;
// Si le fichier n'existe pas, rien à nettoyer
if (!file_exists($DEDUP_FILE)) {
return;
}
$now = time(); $now = time();
$lines = file($DEDUP_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$kept = []; $kept = [];
$has_changed = false;
// Lecture du fichier
$lines = file($DEDUP_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return;
}
foreach ($lines as $line) { foreach ($lines as $line) {
$parts = explode('|', $line); $parts = explode('|', $line);
if (count($parts) >= 2 && ($now - (int)$parts[1]) <= $DEDUP_WINDOW) {
$kept[] = $line; // On vérifie si la ligne est valide et si le timestamp (index 1) est encore dans la fenêtre
if (count($parts) >= 2) {
$timestamp = (int)$parts[1];
if (($now - $timestamp) <= (int)$DEDUP_WINDOW) {
$kept[] = $line;
} else {
$has_changed = true; // On a trouvé au moins une ligne à supprimer
}
} }
} }
file_put_contents($DEDUP_FILE, implode("\n", $kept) . (empty($kept) ? "" : "\n"));
// On ne réécrit le fichier que si des lignes ont été supprimées
if ($has_changed) {
$content = implode("\n", $kept);
if (!empty($content)) {
$content .= "\n";
}
// LOCK_EX évite que deux instances n'écrivent en même temps
file_put_contents($DEDUP_FILE, $content, LOCK_EX);
}
} }
/** /**
* Vérifie si une alerte doit être envoyée (Déduplication) * Vérifie si une alerte doit être envoyée (Déduplication)
* La clé attendue est : "hostname|app|level|event"
*/ */
function should_notify_dedup($key) { function should_notify_dedup(string $key): bool {
global $DEDUP_FILE, $DEDUP_WINDOW; global $DEDUP_FILE, $DEDUP_WINDOW;
if (!file_exists($DEDUP_FILE)) return true;
if (!file_exists($DEDUP_FILE)) {
return true;
}
$now = time(); $now = time();
$last_ts = 0; $last_ts = 0;
$handle = fopen($DEDUP_FILE, 'r'); $handle = fopen($DEDUP_FILE, 'r');
if (!$handle) {
return true; // En cas d'erreur de lecture, on autorise l'alerte par sécurité
}
// On parcourt le fichier
while (($line = fgets($handle)) !== false) { while (($line = fgets($handle)) !== false) {
$p = explode('|', trim($line)); $line = trim($line);
if (empty($line)) continue;
$p = explode('|', $line);
// Format du fichier : host|timestamp|app|level|event
// On reconstruit la clé de comparaison (sans le timestamp index 1)
if (count($p) >= 5) { if (count($p) >= 5) {
$current_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}"; $row_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}";
if ($current_key === $key) {
if ($row_key === $key) {
$last_ts = (int)$p[1]; $last_ts = (int)$p[1];
} }
} }
} }
fclose($handle); fclose($handle);
return ($now - $last_ts) >= $DEDUP_WINDOW; // Calcul de l'écart : vrai si on a dépassé la fenêtre ou si jamais vu (last_ts = 0)
return ($now - $last_ts) >= (int)$DEDUP_WINDOW;
} }
/** /**
* Envoi vers ntfy * Envoi vers ntfy
*/ */
@@ -123,26 +170,28 @@ function send_mail($subject, $body) {
* Traitement d'une ligne de log * Traitement d'une ligne de log
*/ */
function process_line($line) { function process_line($line) {
global $CONFIG, $DEDUP_FILE; global $CONFIG, $DEDUP_FILE;
$data = json_decode($line, true); $data = json_decode($line, true);
if (!$data || !isset($data['level'], $data['event'])) return; if (!$data || !isset($data['level'], $data['event'])) return;
$level = strtoupper($data['level']); $level = strtoupper($data['level']);
$event = $data['event']; $event = $data['event'];
if (in_array($level, ['DEBUG', 'INFO', 'NOTICE'])) return; // On garde uniquement l'ignore list explicite pour les événements
if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return; if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return;
// Déduplication // Déduplication
$key = "{$data['host']}|{$data['app']}|{$level}|{$event}"; $key = "{$data['host']}|{$data['app']}|{$level}|{$event}";
if (!should_notify_dedup($key)) { if (!should_notify_dedup($key)) {
log_debug("alert_suppressed_dedup", "Alerte dédupliquée", ["event=$event", "host={$data['host']}"]);
return; return;
} }
// Détermination des canaux (Règle spécifique puis défaut) // Détermination des canaux
$channels_str = $CONFIG['RULES'][$event] ?? $CONFIG['DEFAULT_CHANNELS'][$level] ?? ''; $channels_str = $CONFIG['RULES'][$event] ?? $CONFIG['DEFAULT_CHANNELS'][$level] ?? '';
// Si aucun canal n'est défini pour ce niveau, ALORS on s'arrête
if (empty($channels_str)) return; if (empty($channels_str)) return;
$channels = explode(',', $channels_str); $channels = explode(',', $channels_str);
$title = "{$data['host']} [{$data['app']}] $level $event"; $title = "{$data['host']} [{$data['app']}] $level $event";

View File

@@ -11,43 +11,51 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details. # GNU Affero General Public License for more details.
set -u set -u
. /opt/monitoring/lib/monitoring-lib.sh || exit 3 # --- Configuration (Seuils par défaut) ---
WARNING=80
WARNING=80 CRITICAL=95
CRITICAL=95 MOUNTS=("/" "/var" "/home")
MOUNTS=("/" "/var" "/home") LOG_BIN="/opt/monitoring/bin/log-cli.php"
for mount in "${MOUNTS[@]}"; do # --- Vérification ROOT ---
# On vérifie si le point de montage existe avant de tester if [ "${EUID}" -ne 0 ]; then
if ! mountpoint -q "$mount"; then echo "ERREUR : Ce script doit être exécuté en tant que root." >&2
continue $LOG_BIN ERROR "internal_error" "Tentative d'exécution sans privilèges root."
exit 1
fi
for mount in "${MOUNTS[@]}"; do
if ! mountpoint -q "$mount"; then continue; fi
# --- 1. Espace Disque ---
used_pct="$(df -P "$mount" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')"
if [[ ! "$used_pct" =~ ^[0-9]+$ ]]; then
$LOG_BIN ERROR "check_failed" "Erreur lecture disque $mount."
else
if [ "$used_pct" -ge "$CRITICAL" ]; then
$LOG_BIN CRITICAL "disk_usage_critical" "Disque $mount critique : $used_pct% utilisé."
elif [ "$used_pct" -ge "$WARNING" ]; then
$LOG_BIN WARNING "disk_usage_high" "Disque $mount élevé : $used_pct% utilisé."
else
$LOG_BIN INFO "disk_ok" "Disque $mount OK : $used_pct% utilisé."
fi
fi fi
used_pct="$(df -P "$mount" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')" # --- 2. Inodes (Déplacé à l'intérieur de la boucle) ---
inode_pct="$(df -iP "$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" if [[ ! "$inode_pct" =~ ^[0-9]+$ ]]; then
continue $LOG_BIN ERROR "check_failed" "Erreur lecture inodes $mount."
fi else
if [ "$inode_pct" -ge "$CRITICAL" ]; then
level="$(threshold_level "$used_pct" "$WARNING" "$CRITICAL")" $LOG_BIN CRITICAL "inode_usage_critical" "Inodes $mount critiques ($inode_pct%)."
elif [ "$inode_pct" -ge "$WARNING" ]; then
case "$level" in $LOG_BIN WARNING "inode_usage_high" "Inodes $mount élevés ($inode_pct%)."
INFO) else
log_info "disk_ok" "Utilisation disque normale" \ $LOG_BIN INFO "inode_ok" "Inodes $mount OK ($inode_pct%)."
"mount=$mount" "used_pct=$used_pct" "warning=$WARNING" "critical=$CRITICAL" fi
;; fi
WARNING) done
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,66 @@
#!/bin/bash
#
# 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.
LOG_BIN="/opt/monitoring/bin/log-cli.php"
# --- Vérification ROOT ---
if [ "${EUID}" -ne 0 ]; then
echo "ERREUR : Ce script doit être exécuté en tant que root." >&2
$LOG_BIN ERROR "internal_error" "Tentative d'exécution sans privilèges root."
exit 1
fi
# --- Vérification et installation de smartctl ---
if ! command -v smartctl >/dev/null 2>&1; then
# On tente l'installation (nécessite root, ce qui est le cas via cron)
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y smartmontools
fi
# Re-vérification après tentative
if ! command -v smartctl >/dev/null 2>&1; then
$LOG_BIN ERROR "internal_error" "smartctl non trouvé et installation impossible."
exit 1
fi
fi
# On récupère les disques qui ont un transport physique (SATA, NVMe, USB)
# Cela exclut d'office les /dev/mapper, /dev/dm-X, /dev/loopX
DISKS=$(lsblk -dno NAME,TRAN | awk '$2!="" {print "/dev/"$1}')
for disk in $DISKS; do
# Vérification : est-ce que smartctl peut lire ce périphérique ?
# --scan-open vérifie si le disque est capable de répondre
if ! smartctl -i "$disk" | grep -q "SMART support is: Enabled" 2>/dev/null; then
# On peut logguer en INFO que le disque est ignoré car non-SMART (ex: clé USB basique)
continue
fi
# 1. État de santé global
smart_output=$(smartctl -H "$disk" 2>/dev/null)
exit_code=$?
if [ $exit_code -ne 0 ]; then
$LOG_BIN CRITICAL "smart_health_bad" "État de santé PHYSIQUE CRITIQUE sur $disk"
else
# 2. Température
temp=$(smartctl -A "$disk" 2>/dev/null | awk '/Temperature_Celsius/ {print $10}' | head -n 1)
[ -z "$temp" ] && temp=$(smartctl -a "$disk" 2>/dev/null | awk '/Temperature:/ {print $2}' | head -n 1)
if [ -n "$temp" ]; then
if [ "$temp" -ge 60 ]; then
$LOG_BIN WARNING "disk_temp_high" "Surchauffe physique sur $disk : ${temp}°C"
fi
fi
$LOG_BIN INFO "smart_health_ok" "Disque physique $disk sain."
fi
done

View File

@@ -11,51 +11,51 @@ LOG_DIR="/var/log/monitoring"
STATE_DIR="/var/lib/monitoring" STATE_DIR="/var/lib/monitoring"
LOCK_DIR="/var/lock/monitoring" LOCK_DIR="/var/lock/monitoring"
TMP_DIR="/tmp/monitoring-install" TMP_DIR="/tmp/monitoring-install"
# Journal des fichiers installés pour déinstallation/état des lieux
INSTALLED_LOG="${STATE_DIR}/installed-files.log"
UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring" UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring"
MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt" MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
INSTALL_DEPS="${INSTALL_DEPS:-true}" INSTALL_DEPS="${INSTALL_DEPS:-true}"
# --- Fonctions --- # --- Fonctions d'affichage ---
info() { echo -e "\e[34m[INFO]\e[0m $1"; }
ok() { echo -e "\e[32m[OK]\e[0m $1"; }
warn() { echo -e "\e[33m[WARN]\e[0m $1"; }
err() { echo -e "\e[31m[ERR]\e[0m $1"; }
# --- Fonctions Techniques ---
require_root() { require_root() {
if [ "${EUID}" -ne 0 ]; then if [ "${EUID}" -ne 0 ]; then
echo "ERREUR: Ce script doit être exécuté en root." >&2 err "Ce script doit être exécuté en root."
exit 1 exit 1
fi fi
} }
install_deps() { install_deps() {
if [ "${INSTALL_DEPS}" != "true" ]; then if [ "${INSTALL_DEPS}" != "true" ]; then return 0; fi
return 0 info "Vérification des dépendances système..."
fi
echo "--- Installation des dépendances ---"
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update apt-get update -qq
apt-get install -y curl coreutils findutils grep sed gawk util-linux ca-certificates apt-get install -y -qq curl coreutils findutils grep sed gawk util-linux ca-certificates php-cli php-curl php-common smartmontools > /dev/null
# Ajout des modules PHP nécessaires pour vos scripts (curl pour ntfy) ok "Dépendances installées."
apt-get install -y php-cli php-curl php-common
else
echo "AVERTISSEMENT: Gestionnaire de paquets apt non détecté. Assurez-vous que php-cli et php-curl sont installés."
fi fi
} }
prepare_dirs() { prepare_dirs() {
echo "--- Préparation des répertoires ---" info "Préparation de l'arborescence dans ${BASE_DIR}..."
mkdir -p "${BASE_DIR}/bin" "${BASE_DIR}/lib" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}" mkdir -p "${BASE_DIR}/bin" "${BASE_DIR}/lib" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}"
chmod 755 "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" touch "$INSTALLED_LOG"
} }
fetch_manifest() { fetch_manifest() {
echo "--- Récupération du manifeste ---" info "Téléchargement du manifeste distant..."
curl -fsS "${MANIFEST_URL}" -o "${TMP_DIR}/manifest.txt" curl -fsS "${MANIFEST_URL}" -o "${TMP_DIR}/manifest.txt"
} }
validate_manifest() { validate_manifest() {
# Validation du format : Hash Mode Chemin
# Exemple : a1b2... 755 bin/script.php
awk ' awk '
NF == 3 && NF == 3 &&
$1 ~ /^[0-9a-fA-F]{64}$/ && $1 ~ /^[0-9a-fA-F]{64}$/ &&
@@ -65,35 +65,32 @@ validate_manifest() {
' "${TMP_DIR}/manifest.txt" ' "${TMP_DIR}/manifest.txt"
} }
download_one() { download_and_install() {
local expected_hash="$1" local expected_hash=$1 mode=$2 rel_path=$3
local mode="$2"
local rel_path="$3"
local url="${UPDATE_BASE_URL}/${rel_path}"
local dst="${BASE_DIR}/${rel_path}" local dst="${BASE_DIR}/${rel_path}"
# On ignore le téléchargement si c'est un fichier de conf qui existe déjà if [ -f "$dst" ]; then
if [[ "$rel_path" == conf/* ]] && [ -f "$dst" ]; then local current_hash
echo "Skip: $rel_path (existe déjà)" current_hash=$(sha256sum "$dst" | awk '{print $1}')
return 0 [ "$current_hash" == "$expected_hash" ] && return 0
info "Mise à jour : $rel_path"
else
info "Installation : $rel_path"
fi fi
echo "Téléchargement: $rel_path"
local tmp_file local tmp_file
tmp_file="$(mktemp "${TMP_DIR}/file.XXXXXX")" tmp_file="$(mktemp "${TMP_DIR}/file.XXXXXX")"
if ! curl -fsS "$url" -o "$tmp_file"; then if ! curl -fsS "${UPDATE_BASE_URL}/${rel_path}" -o "$tmp_file"; then
echo "ERREUR: Échec du téléchargement de ${url}" >&2 err "Échec du téléchargement pour $rel_path"
rm -f "$tmp_file" rm -f "$tmp_file"
return 1 return 1
fi fi
local got_hash local got_hash
got_hash="$(sha256sum "$tmp_file" | awk '{print $1}')" got_hash=$(sha256sum "$tmp_file" | awk '{print $1}')
if [ "$got_hash" != "$expected_hash" ]; then if [ "$got_hash" != "$expected_hash" ]; then
echo "ERREUR: Hash invalide pour ${rel_path}" >&2 err "Hash invalide pour $rel_path"
rm -f "$tmp_file" rm -f "$tmp_file"
return 1 return 1
fi fi
@@ -103,34 +100,45 @@ download_one() {
chmod "$mode" "$dst" chmod "$mode" "$dst"
} }
install_from_manifest() { # --- NOUVEAUTÉ : Gestion du journal et purge propre ---
echo "--- Installation des fichiers ---"
while read -r hash mode rel_path; do update_installed_log() {
[ -n "${hash:-}" ] || continue # On sauvegarde la liste des chemins relatifs du manifeste validé dans le journal permanent
download_one "$hash" "$mode" "$rel_path" awk '{print $3}' "${TMP_DIR}/manifest-valid.txt" > "$INSTALLED_LOG"
done < "${TMP_DIR}/manifest-valid.txt" ok "Journal des fichiers déployés mis à jour ($INSTALLED_LOG)."
} }
show_next_steps() { purge_obsolete_files() {
cat <<EOF info "Analyse des fichiers obsolètes (Synchronisation avec le journal)..."
# On compare ce qui était installé (journal) avec ce qui est dans le nouveau manifeste
if [ ! -s "$INSTALLED_LOG" ]; then
warn "Journal vide, passage en mode scan classique."
# Fallback sur le scan de dossier si le journal n'existe pas encore
find "${BASE_DIR}/bin" "${BASE_DIR}/lib" "${BASE_DIR}/conf" -type f 2>/dev/null | while read -r local_file; do
local rel_path="${local_file#$BASE_DIR/}"
[[ "$rel_path" == *".local."* ]] && continue
if ! grep -qw "$rel_path" "${TMP_DIR}/manifest-valid.txt"; then
warn "Suppression : $rel_path"
rm -f "$local_file"
fi
done
return
fi
Installation terminée avec succès dans ${BASE_DIR}. # Mode Journal : On lit l'ancien journal pour voir ce qui doit disparaître
while read -r old_file; do
Étapes suivantes : # Si le fichier du journal n'est plus dans le nouveau manifeste
1. Configurez vos alertes : if ! grep -qw "$old_file" "${TMP_DIR}/manifest-valid.txt"; then
cp ${CONF_DIR}/alert-engine.conf.php ${CONF_DIR}/alert-engine.local.conf.php if [ -f "${BASE_DIR}/$old_file" ]; then
nano ${CONF_DIR}/alert-engine.local.conf.php # Protection ultime des fichiers .local (au cas où ils auraient été loggués par erreur)
if [[ "$old_file" != *".local."* ]]; then
2. Initialisez la configuration globale : warn "Suppression du fichier obsolète : $old_file"
cp ${CONF_DIR}/monitoring.conf.php ${CONF_DIR}/monitoring.local.conf.php rm -f "${BASE_DIR}/$old_file"
fi
3. Lancez un audit des configurations : fi
php ${BASE_DIR}/bin/monitoring-update-config.php fi
done < "$INSTALLED_LOG"
4. Planifiez les tâches (cron) :
*/5 * * * * php ${BASE_DIR}/bin/alert-engine.php
10 3 * * * php ${BASE_DIR}/bin/monitoring-update.php
EOF
} }
# --- Main --- # --- Main ---
@@ -142,16 +150,38 @@ main() {
fetch_manifest fetch_manifest
if ! validate_manifest > "${TMP_DIR}/manifest-valid.txt"; then if ! validate_manifest > "${TMP_DIR}/manifest-valid.txt"; then
echo "ERREUR: Le manifeste est invalide ou corrompu." >&2 err "Le manifeste est invalide ou corrompu."
exit 1 exit 1
fi fi
install_from_manifest echo "--------------------------------------------------"
info "Phase 1 : Installation et mises à jour"
# Nettoyage while read -r hash mode rel_path; do
[ -n "${hash:-}" ] || continue
download_and_install "$hash" "$mode" "$rel_path"
done < "${TMP_DIR}/manifest-valid.txt"
echo "--------------------------------------------------"
info "Phase 2 : Nettoyage et Journalisation"
purge_obsolete_files
update_installed_log
rm -rf "${TMP_DIR}" rm -rf "${TMP_DIR}"
echo "--------------------------------------------------"
ok "Opération terminée avec succès."
show_next_steps # --- Vérification de la configuration ---
local_conf="${CONF_DIR}/monitoring.local.conf.php"
orig_conf="${CONF_DIR}/monitoring.conf.php"
if [ -f "$local_conf" ] && [ -f "$orig_conf" ]; then
if [ "$(sha256sum "$local_conf" | awk '{print $1}')" == "$(sha256sum "$orig_conf" | awk '{print $1}')" ]; then
echo -e "\n\e[33m[ATTENTION]\e[0m Votre fichier de configuration est identique à l'original."
warn "Pensez à éditer ${local_conf}."
else
ok "Configuration locale personnalisée détectée."
fi
fi
} }
main "$@" main "$@"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
/**
* Pont de logging pour scripts Bash
*/
require_once __DIR__ . '/../lib/monitoring-lib.php';
if ($argc < 4) {
fwrite(STDERR, "Usage: log-cli.php <LEVEL> <EVENT> <MESSAGE> [CONTEXT...]\n");
exit(1);
}
$level = strtoupper($argv[1]);
$event = $argv[2];
$message = $argv[3];
$context = [];
// On récupère les arguments restants comme contexte
for ($i = 4; $i < $argc; $i++) {
$context[] = $argv[$i];
}
log_event($level, $event, $message, $context);

View File

324
servers/linux/monitoring/bin/monitoring-update.php Normal file → Executable file
View File

@@ -1,219 +1,179 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Moteur de mise à jour des programmes * Moteur de mise à jour
* Pilotage du script Bash + Initialisation des Configs + Cron + Ménage
* Copyright (C) 2026 Cédric Abonnel * Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
*/ */
require_once __DIR__ . '/../lib/monitoring-lib.php'; require_once __DIR__ . '/../lib/monitoring-lib.php';
// --- Chargement de la configuration spécifique --- // Sécurité : Un seul update à la fois
foreach (["/opt/monitoring/conf/autoupdate.conf.php", "/opt/monitoring/conf/autoupdate.local.conf.php"] as $conf) {
if (file_exists($conf)) {
$extra_conf = include $conf;
if (is_array($extra_conf)) {
$CONFIG = array_replace_recursive($CONFIG, $extra_conf);
}
}
}
// Variables par défaut
$UPDATE_ENABLED = $CONFIG['UPDATE_ENABLED'] ?? true;
$UPDATE_TMP_DIR = $CONFIG['UPDATE_TMP_DIR'] ?? '/tmp/monitoring-update';
$UPDATE_TIMEOUT_CONNECT = $CONFIG['UPDATE_TIMEOUT_CONNECT'] ?? 3;
$UPDATE_TIMEOUT_TOTAL = $CONFIG['UPDATE_TIMEOUT_TOTAL'] ?? 15;
$UPDATE_MANIFEST_URL = $CONFIG['UPDATE_MANIFEST_URL'] ?? '';
$UPDATE_BASE_URL = $CONFIG['UPDATE_BASE_URL'] ?? '';
$UPDATE_ALLOW_DELETE = $CONFIG['UPDATE_ALLOW_DELETE'] ?? false;
// Sécurité
lock_or_exit("monitoring-update"); lock_or_exit("monitoring-update");
if (!$UPDATE_ENABLED) { echo "\e[1m--- Début de la mise à jour système ---\e[0m\n";
log_notice("update_disabled", "Mise à jour désactivée par configuration");
exit(0); // 1. Avant l'update, on mémorise la liste des fichiers actuellement installés (L'AVANT)
$installed_log = $CONFIG['INSTALLED_LOG'] ?? '/var/lib/monitoring/installed-files.log';
$old_installed_files = [];
if (file_exists($installed_log)) {
$old_installed_files = file($installed_log, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
} }
if (!is_dir($UPDATE_TMP_DIR)) { // 2. Exécution du moteur de synchronisation Bash
mkdir($UPDATE_TMP_DIR, 0755, true); $install_script = __DIR__ . '/install-monitoring.sh';
if (!file_exists($install_script)) {
log_error("update_script_missing", "Script d'installation introuvable", ["path" => $install_script]);
echo "\e[31m[ERR]\e[0m Script d'installation introuvable : $install_script\n";
exit(1);
} }
// Exécution du moteur de synchronisation Bash
$command = "bash " . escapeshellarg($install_script) . " --auto 2>&1";
$handle = popen($command, 'r');
if ($handle) {
while (!feof($handle)) {
$line = fgets($handle);
if ($line) echo $line;
}
$exit_code = pclose($handle);
} else {
$exit_code = 1;
}
if ($exit_code !== 0) {
log_error("update_failed", "Le script Bash a échoué");
exit(1);
}
// 3. Après l'update, on récupère la nouvelle liste (L'APRÈS)
$new_installed_files = [];
if (file_exists($installed_log)) {
$new_installed_files = file($installed_log, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
}
// Détermination des fichiers qui ont été SUPPRIMÉS lors de cette mise à jour (dynamique)
$deleted_files = array_diff($old_installed_files, $new_installed_files);
echo "\e[1m--- Finalisation des configurations ---\e[0m\n";
ensure_local_configs();
ensure_crontab_entries($deleted_files);
echo "\e[32m[OK]\e[0m Système à jour.\n";
/** /**
* Télécharge et valide le manifeste * Nettoyage et Mise à jour du Crontab
* @param array $deleted_files Liste des chemins de fichiers supprimés du déploiement
*/ */
function fetch_manifest() { function ensure_crontab_entries($deleted_files) {
global $UPDATE_MANIFEST_URL, $UPDATE_TIMEOUT_TOTAL; global $CONFIG, $MONITORING_BASE_DIR;
$content = @file_get_contents($UPDATE_MANIFEST_URL, false, stream_context_create([ // --- CONFIGURATION DU MÉNAGE STATIQUE ---
'http' => ['timeout' => $UPDATE_TIMEOUT_TOTAL] // On ajoute ici les vieux chemins orphelins à éjecter impérativement
])); $static_cleanup_patterns = [
'/usr/local/bin/sys_check.sh',
'bin/check_disk.php', // Ancienne erreur de nommage
'bin/check-disk.sh' // Ancienne erreur de séparateur
];
if ($content === false) { // Préparation des jobs requis
log_error("manifest_download_failed", "Impossible de télécharger le manifeste", ["url=$UPDATE_MANIFEST_URL"]); $required_jobs = array_map(function($job) use ($MONITORING_BASE_DIR) {
return false; return str_replace('{BASE_DIR}', $MONITORING_BASE_DIR, $job);
} }, $CONFIG['CRON_JOBS'] ?? [
"*/5 * * * * bash {$MONITORING_BASE_DIR}/bin/check_disk.sh > /dev/null 2>&1",
"*/15 * * * * bash {$MONITORING_BASE_DIR}/bin/check_smart.sh > /dev/null 2>&1",
"10 3 * * * php {$MONITORING_BASE_DIR}/bin/monitoring-update.php > /dev/null 2>&1",
"* * * * * php {$MONITORING_BASE_DIR}/bin/alert-engine.php > /dev/null 2>&1"
]);
$lines = explode("\n", trim($content)); $current_cron = shell_exec("crontab -l 2>/dev/null") ?: "";
$manifest_data = []; $lines = explode("\n", trim($current_cron));
$new_lines = [];
$has_changed = false;
// --- PHASE A : Nettoyage (Dynamique + Statique) ---
foreach ($lines as $line) { foreach ($lines as $line) {
if (empty(trim($line))) continue; $trim_line = trim($line);
if (empty($trim_line)) continue;
$parts = preg_split('/\s+/', trim($line));
if (count($parts) !== 3) continue;
list($hash, $mode, $path) = $parts; $keep = true;
// Validation stricte (Regex identique au Bash) // 1. Nettoyage Dynamique (basé sur le log Git)
if (preg_match('/^[0-9a-f]{64}$/i', $hash) && foreach ($deleted_files as $deleted_path) {
preg_match('/^(644|755)$/', $mode) && if (strpos($trim_line, $deleted_path) !== false) {
preg_match('/^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/', $path) && echo "\e[33m[CLEAN]\e[0m Script supprimé du déploiement : " . basename($deleted_path) . "\n";
strpos($path, '..') === false) { $keep = false;
$manifest_data[] = ['hash' => strtolower($hash), 'mode' => $mode, 'path' => $path]; $has_changed = true;
} break;
}
if (empty($manifest_data)) {
log_error("manifest_invalid", "Le manifeste distant est vide ou invalide");
return false;
}
log_info("manifest_downloaded", "Manifeste téléchargé", ["url=$UPDATE_MANIFEST_URL"]);
return $manifest_data;
}
/**
* Mise à jour d'un fichier unique
*/
function update_one_file($expected_hash, $mode, $rel_path) {
global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL;
$local_file = $MONITORING_BASE_DIR . '/' . $rel_path;
$remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path;
$tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . bin2hex(random_bytes(4));
$local_hash = file_exists($local_file) ? hash_file('sha256', $local_file) : null;
if ($local_hash === $expected_hash) {
log_debug("update_not_needed", "Fichier déjà à jour", ["file=$rel_path"]);
return true;
}
// Téléchargement via cURL pour gérer les timeouts proprement
$ch = curl_init($remote_url);
$fp = fopen($tmp_file, 'w');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$success = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if (!$success || $http_code !== 200) {
log_error("update_download_failed", "Téléchargement impossible", ["file=$rel_path", "url=$remote_url"]);
@unlink($tmp_file);
return false;
}
$downloaded_hash = hash_file('sha256', $tmp_file);
if ($downloaded_hash !== $expected_hash) {
log_error("update_hash_mismatch", "Hash téléchargé invalide", ["file=$rel_path", "expected=$expected_hash", "got=$downloaded_hash"]);
@unlink($tmp_file);
return false;
}
// Installation
ensure_parent_dir($local_file);
chmod($tmp_file, ($mode == '755' ? 0755 : 0644));
if (rename($tmp_file, $local_file)) {
if (!$local_hash) {
log_notice("file_created", "Fichier créé", ["file=$rel_path", "mode=$mode"]);
} else {
log_notice("update_applied", "Mise à jour appliquée", ["file=$rel_path", "new_hash=$expected_hash"]);
}
return true;
}
return false;
}
/**
* Suppression des fichiers obsolètes
*/
function delete_extra_files($manifest_paths) {
global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR;
if (!$UPDATE_ALLOW_DELETE) return;
$dirs = ['bin', 'lib', 'conf'];
foreach ($dirs as $dir) {
$full_dir = $MONITORING_BASE_DIR . '/' . $dir;
if (!is_dir($full_dir)) continue;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($full_dir));
foreach ($iterator as $file) {
if ($file->isDir()) continue;
$rel_path = str_replace($MONITORING_BASE_DIR . '/', '', $file->getPathname());
// Protection des .local.conf
if (str_ends_with($rel_path, '.local.conf') || str_ends_with($rel_path, '.local.php')) {
continue;
} }
}
if (!in_array($rel_path, $manifest_paths)) {
if (@unlink($file->getPathname())) { // 2. Nettoyage Statique (basé sur ta liste forcée)
log_notice("file_deleted", "Fichier obsolète supprimé", ["file=$rel_path"]); if ($keep) {
foreach ($static_cleanup_patterns as $pattern) {
if (strpos($trim_line, $pattern) !== false) {
echo "\e[33m[CLEAN]\e[0m Suppression du résidu historique : $pattern\n";
$keep = false;
$has_changed = true;
break;
} }
} }
} }
if ($keep) $new_lines[] = $trim_line;
} }
}
/** // --- PHASE B : Ajout des nouveaux jobs ---
* Lancement du script de synchronisation local foreach ($required_jobs as $job) {
*/ // Extraction du chemin du script pour éviter les doublons
function run_local_conf_sync() { preg_match('/\/bin\/([a-z0-9_-]+\.(php|sh))/i', $job, $matches);
global $MONITORING_BASE_DIR; $script_path = $matches[0] ?? "";
$sync_script = $MONITORING_BASE_DIR . '/bin/monitoring-update-config.sh'; // On cherche la version PHP
if (file_exists($sync_script)) { $found = false;
log_info("local_conf_sync_start", "Synchronisation des configs locales"); foreach ($new_lines as $line) {
passthru("php " . escapeshellarg($sync_script), $return_var); if (strpos($line, $script_path) !== false) {
if ($return_var !== 0) { $found = true;
log_warning("local_conf_sync_failed", "Échec de synchronisation"); break;
}
}
if (!$found && !empty($script_path)) {
$new_lines[] = $job;
$has_changed = true;
echo "\e[32m[OK]\e[0m Ajout au cron : " . basename($script_path) . "\n";
} }
} }
}
// --- Exécution principale --- // --- PHASE C : Application ---
if ($has_changed) {
$manifest = fetch_manifest(); $content = implode("\n", array_filter($new_lines, 'trim')) . "\n";
if (!$manifest) exit(2); $tmp_cron = tempnam(sys_get_temp_dir(), 'cron');
file_put_contents($tmp_cron, $content);
$total = count($manifest); exec("crontab " . escapeshellarg($tmp_cron));
$updated = 0; unlink($tmp_cron);
$failed = 0; echo "\e[32m[OK]\e[0m Crontab synchronisé.\n";
$remote_paths = [];
foreach ($manifest as $item) {
$remote_paths[] = $item['path'];
if (update_one_file($item['hash'], $item['mode'], $item['path'])) {
$updated++;
} else { } else {
$failed++; echo "[INFO] Crontab déjà à jour.\n";
} }
} }
delete_extra_files($remote_paths); function ensure_local_configs() {
run_local_conf_sync(); global $MONITORING_CONF_DIR;
$configs = [
if ($failed > 0) { 'monitoring.conf.php' => 'monitoring.local.conf.php',
log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total=$total", "failed=$failed"]); 'alert-engine.conf.php' => 'alert-engine.conf.local.php',
} else { 'autoupdate.conf.php' => 'autoupdate.local.conf.php'
log_info("update_finished", "Mise à jour terminée", ["total=$total", "checked=$updated"]); ];
} foreach ($configs as $src => $dst) {
$dst_path = $MONITORING_CONF_DIR . '/' . $dst;
exit_with_status(); if (!file_exists($dst_path)) {
$src_path = $MONITORING_CONF_DIR . '/' . $src;
if (file_exists($src_path)) {
copy($src_path, $dst_path);
chmod($dst_path, 0600);
}
}
}
}

177
servers/linux/monitoring/bin/monitoring.php Normal file → Executable file
View File

@@ -8,40 +8,36 @@
require_once __DIR__ . '/../lib/monitoring-lib.php'; require_once __DIR__ . '/../lib/monitoring-lib.php';
// --- Configuration --- // --- Configuration ---
// On s'appuie sur le chargement de la lib, mais on surcharge si nécessaire // Note : La lib a déjà chargé $CONFIG['UPDATE_BASE_URL'] etc. depuis monitoring.local.conf.php
$conf_file = "/opt/monitoring/conf/autoupdate.conf.php"; // Format PHP recommandé // On ne charge les fichiers spécifiques que s'ils apportent des règles de mise à jour uniques.
// --- Chargement de la configuration spécifique ---
foreach (["/opt/monitoring/conf/autoupdate.conf.php", "/opt/monitoring/conf/autoupdate.local.conf.php"] as $conf) { foreach (["/opt/monitoring/conf/autoupdate.conf.php", "/opt/monitoring/conf/autoupdate.local.conf.php"] as $conf) {
if (file_exists($conf)) { if (file_exists($conf)) {
$extra_conf = include $conf; $extra_conf = include $conf;
if (is_array($extra_conf)) { if (is_array($extra_conf)) {
$CONFIG = array_replace_recursive($CONFIG, $extra_conf); $CONFIG = array_replace_recursive($CONFIG, $extra_conf);
} }
} }
} }
// Variables par défaut // Variables par défaut (fallback si absent de la config globale)
$UPDATE_ENABLED = $CONFIG['UPDATE_ENABLED'] ?? true; $UPDATE_ENABLED = $CONFIG['UPDATE_ENABLED'] ?? true;
$UPDATE_TMP_DIR = $CONFIG['UPDATE_TMP_DIR'] ?? '/tmp/monitoring-update'; $UPDATE_TMP_DIR = $CONFIG['UPDATE_TMP_DIR'] ?? '/tmp/monitoring-update';
$UPDATE_TIMEOUT_CONNECT = $CONFIG['UPDATE_TIMEOUT_CONNECT'] ?? 3;
$UPDATE_TIMEOUT_TOTAL = $CONFIG['UPDATE_TIMEOUT_TOTAL'] ?? 15; $UPDATE_TIMEOUT_TOTAL = $CONFIG['UPDATE_TIMEOUT_TOTAL'] ?? 15;
$UPDATE_MANIFEST_URL = $CONFIG['UPDATE_MANIFEST_URL'] ?? ''; $UPDATE_MANIFEST_URL = $CONFIG['UPDATE_MANIFEST_URL'] ?? '';
$UPDATE_BASE_URL = $CONFIG['UPDATE_BASE_URL'] ?? ''; $UPDATE_BASE_URL = $CONFIG['UPDATE_BASE_URL'] ?? '';
$UPDATE_ALLOW_DELETE = $CONFIG['UPDATE_ALLOW_DELETE'] ?? false; $UPDATE_ALLOW_DELETE = $CONFIG['UPDATE_ALLOW_DELETE'] ?? false;
$MONITORING_BASE_DIR = $CONFIG['MONITORING_BASE_DIR'] ?? '/opt/monitoring'; $MONITORING_BASE_DIR = $MONITORING_BASE_DIR; // Provient de la lib
// --- Initialisation --- // --- Initialisation ---
lock_or_exit("monitoring-update"); lock_or_exit("monitoring-update");
if (!$UPDATE_ENABLED) { if (!$UPDATE_ENABLED) {
log_notice("update_disabled", "Mise à jour désactivée par configuration"); log_notice("update_disabled", "Mise à jour désactivée");
exit(0); exit(0);
} }
if (!is_dir($UPDATE_TMP_DIR)) { if (!is_dir($UPDATE_TMP_DIR)) {
if (!mkdir($UPDATE_TMP_DIR, 0755, true)) { mkdir($UPDATE_TMP_DIR, 0755, true);
fail_internal("Impossible de créer le répertoire temporaire: $UPDATE_TMP_DIR");
}
} }
/** /**
@@ -51,13 +47,16 @@ function fetch_manifest($url) {
global $UPDATE_TIMEOUT_TOTAL; global $UPDATE_TIMEOUT_TOTAL;
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt_array($ch, [
curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL); CURLOPT_RETURNTRANSFER => true,
curl_setopt($ch, CURLOPT_FAILONERROR, true); CURLOPT_TIMEOUT => $UPDATE_TIMEOUT_TOTAL,
CURLOPT_FAILONERROR => true,
CURLOPT_FOLLOWLOCATION => true
]);
$content = curl_exec($ch); $content = curl_exec($ch);
if (curl_errno($ch)) { if (curl_errno($ch)) {
log_error("manifest_download_failed", "Impossible de télécharger le manifeste", ["url" => $url, "error" => curl_error($ch)]); log_error("manifest_download_failed", "Échec téléchargement manifeste", ["url" => $url, "err" => curl_error($ch)]);
return false; return false;
} }
curl_close($ch); curl_close($ch);
@@ -66,30 +65,15 @@ function fetch_manifest($url) {
$lines = explode("\n", trim($content)); $lines = explode("\n", trim($content));
foreach ($lines as $line) { foreach ($lines as $line) {
$line = trim($line); if (preg_match('/^([0-9a-fA-F]{64})\s+(644|755)\s+((bin|lib|conf)\/[A-Za-z0-9._\/-]+)$/', trim($line), $matches)) {
if (empty($line)) continue; $manifest_entries[] = ['hash' => $matches[1], 'mode' => $matches[2], 'path' => $matches[3]];
// Validation format: hash(64) mode(3) path
if (preg_match('/^([0-9a-fA-F]{64})\s+(644|755)\s+((bin|lib|conf)\/[A-Za-z0-9._\/-]+)$/', $line, $matches)) {
$manifest_entries[] = [
'hash' => $matches[1],
'mode' => $matches[2],
'path' => $matches[3]
];
} }
} }
if (empty($manifest_entries)) {
log_error("manifest_invalid", "Le manifeste distant est invalide ou vide", ["url" => $url]);
return false;
}
log_info("manifest_downloaded", "Manifeste téléchargé", ["url" => $url]);
return $manifest_entries; return $manifest_entries;
} }
/** /**
* Met à jour un fichier spécifique * Met à jour un fichier
*/ */
function update_one_file($entry) { function update_one_file($entry) {
global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL; global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL;
@@ -99,129 +83,86 @@ function update_one_file($entry) {
$remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path; $remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path;
$expected_hash = strtolower($entry['hash']); $expected_hash = strtolower($entry['hash']);
// Calcul du hash local actuel if (file_exists($target_file) && hash_file('sha256', $target_file) === $expected_hash) {
$local_hash = file_exists($target_file) ? hash_file('sha256', $target_file) : ""; return true;
if ($local_hash === $expected_hash) {
log_debug("update_not_needed", "Fichier déjà à jour", ["file" => $rel_path]);
return true;
} }
// Téléchargement
$tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . uniqid(); $tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . uniqid();
$ch = curl_init($remote_url); $ch = curl_init($remote_url);
$fp = fopen($tmp_file, 'wb'); $fp = fopen($tmp_file, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt_array($ch, [
curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL); CURLOPT_FILE => $fp,
curl_setopt($ch, CURLOPT_FAILONERROR, true); CURLOPT_TIMEOUT => $UPDATE_TIMEOUT_TOTAL,
CURLOPT_FAILONERROR => true,
CURLOPT_FOLLOWLOCATION => true
]);
$success = curl_exec($ch); $success = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch); curl_close($ch);
fclose($fp); fclose($fp);
if (!$success) { if (!$success || hash_file('sha256', $tmp_file) !== $expected_hash) {
log_error("update_download_failed", "Téléchargement impossible", ["file" => $rel_path, "url" => $remote_url, "error" => $error]); log_error("update_failed", "Fichier invalide ou corrompu", ["file" => $rel_path]);
@unlink($tmp_file); @unlink($tmp_file);
return false; return false;
} }
// Vérification Hash
$downloaded_hash = hash_file('sha256', $tmp_file);
if ($downloaded_hash !== $expected_hash) {
log_error("update_hash_mismatch", "Hash téléchargé invalide", ["file" => $rel_path, "expected" => $expected_hash, "got" => $downloaded_hash]);
@unlink($tmp_file);
return false;
}
// Installation
ensure_parent_dir($target_file); ensure_parent_dir($target_file);
chmod($tmp_file, octdec($entry['mode'])); chmod($tmp_file, octdec($entry['mode']));
safe_mv($tmp_file, $target_file); // Utilise la fonction safe_mv de ta lib
if (!rename($tmp_file, $target_file)) {
fail_internal("Échec du déplacement de $tmp_file vers $target_file");
}
if ($local_hash === "") {
log_notice("file_created", "Fichier créé depuis le manifeste", ["file" => $rel_path, "mode" => $entry['mode']]);
} else {
log_notice("update_applied", "Mise à jour appliquée", ["file" => $rel_path, "old_hash" => $local_hash, "new_hash" => $expected_hash]);
}
log_notice("file_updated", "Mise à jour appliquée", ["file" => $rel_path]);
return true; return true;
} }
/** /**
* Supprime les fichiers locaux absents du manifeste * Nettoyage
*/ */
function delete_extra_files($remote_files) { function delete_extra_files($remote_files) {
global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR; global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR, $SCRIPT_PATH;
if (!$UPDATE_ALLOW_DELETE) return; if (!$UPDATE_ALLOW_DELETE) return;
$directories = ['bin', 'lib', 'conf']; foreach (['bin', 'lib', 'conf'] as $dir) {
foreach ($directories as $dir) { $full_path = $MONITORING_BASE_DIR . '/' . $dir;
$full_path = $MONITORING_BASE_DIR . '/' . $dir; if (!is_dir($full_path)) continue;
if (!is_dir($full_path)) continue;
$iterator = new RecursiveIteratorIterator( $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS));
new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) { foreach ($iterator as $file) {
// On récupère le chemin relatif par rapport à la racine du monitoring $path = $file->getPathname();
$rel_path = substr($file->getPathname(), strlen($MONITORING_BASE_DIR) + 1); $rel_path = substr($path, strlen($MONITORING_BASE_DIR) + 1);
// 1. Protection : Si c'est dans le manifeste distant, on ne touche à rien // PROTECTIONS
if (in_array($rel_path, $remote_files)) { if (in_array($rel_path, $remote_files)) continue;
continue; if (str_contains($rel_path, '.local.')) continue; // Protection fichiers locaux
} if ($path === $SCRIPT_PATH) continue; // Ne pas se suicider
// 2. Protection générique : On n'efface JAMAIS les fichiers de configuration locale if (@unlink($path)) {
// Cela couvre : *.local.conf.php, *.local.conf, et même *.local.php par sécurité log_notice("file_deleted", "Fichier obsolète supprimé", ["file" => $rel_path]);
if (str_ends_with($rel_path, '.local.conf.php') || }
str_ends_with($rel_path, '.local.conf') || }
str_ends_with($rel_path, '.local.php')) { }
log_debug("delete_skipped", "Fichier local protégé (ignoré)", ["file" => $rel_path]);
continue;
}
// 3. Suppression si le fichier est obsolète et non protégé
if (@unlink($file->getPathname())) {
log_notice("file_deleted", "Fichier obsolète supprimé", ["file" => $rel_path]);
} else {
log_error("delete_failed", "Impossible de supprimer le fichier local", ["file" => $rel_path]);
}
}
}
} }
// --- Main --- // --- Main ---
$manifest = fetch_manifest($UPDATE_MANIFEST_URL); $manifest = fetch_manifest($UPDATE_MANIFEST_URL);
if ($manifest === false) exit(2); if (!$manifest) exit(2);
$total = count($manifest);
$updated = 0;
$failed = 0;
$remote_paths = []; $remote_paths = [];
$updated = 0; $failed = 0;
foreach ($manifest as $entry) { foreach ($manifest as $entry) {
$remote_paths[] = $entry['path']; $remote_paths[] = $entry['path'];
if (update_one_file($entry)) { update_one_file($entry) ? $updated++ : $failed++;
$updated++;
} else {
$failed++;
}
} }
delete_extra_files($remote_paths); delete_extra_files($remote_paths);
if ($failed > 0) { if ($failed > 0) {
log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total" => $total, "updated" => $updated, "failed" => $failed]); log_warning("update_partial", "Mise à jour terminée avec erreurs", ["failed" => $failed]);
} else { } else {
log_info("update_finished", "Mise à jour terminée", ["total" => $total, "updated" => $updated]); log_info("update_ok", "Mise à jour terminée avec succès");
} }
exit_with_status(); exit_with_status();

View File

@@ -24,12 +24,6 @@ return [
'ALERT_MAIL_SUBJECT_PREFIX' => '[monitoring]', 'ALERT_MAIL_SUBJECT_PREFIX' => '[monitoring]',
'DEST' => 'admin@example.com', // N'oubliez pas de définir le destinataire 'DEST' => 'admin@example.com', // N'oubliez pas de définir le destinataire
// --- Configuration ntfy ---
'NTFY_SERVER' => 'https://ntfy.sh',
'NTFY_TOPIC' => 'TPOSOB84sBJ6HTZ7',
'NTFY_TOKEN' => '',
'NTFY_CLICK_URL' => '',
// --- Déduplication --- // --- Déduplication ---
'ALERT_DEDUP_WINDOW' => 3600, // en secondes 'ALERT_DEDUP_WINDOW' => 3600, // en secondes
@@ -42,16 +36,25 @@ return [
// --- Canaux par défaut selon le niveau --- // --- Canaux par défaut selon le niveau ---
'DEFAULT_CHANNELS' => [ 'DEFAULT_CHANNELS' => [
'WARNING' => 'ntfy', 'INFO' => 'ntfy',
'ERROR' => 'ntfy,mail', 'NOTICE' => 'ntfy',
'CRITICAL' => 'ntfy,mail', 'WARNING' => 'ntfy',
'ERROR' => 'ntfy,mail',
'CRITICAL' => 'ntfy,mail',
], ],
// --- Tags ntfy par niveau --- // --- Tags : Une icône pour chaque état possible ---
'NTFY_TAGS' => [ 'NTFY_TAGS' => [
'WARNING' => 'warning', 'DEBUG' => 'gear', // ⚙️
'ERROR' => 'warning,rotating_light', 'INFO' => 'information_source', //
'CRITICAL' => 'skull,warning', 'NOTICE' => 'bell', // 🔔
'SUCCESS' => 'white_check_mark', // ✅
'WARNING' => 'warning', // ⚠️
'ERROR' => 'rotating_light,warning', // 🚨
'CRITICAL' => 'skull,warning', // 💀
'ALERT' => 'ambulance,rotating_light',// 🚑
'EMERGENCY' => 'fire,sos,skull', // 🔥
'AUDIT' => 'mag', // 🔍
], ],
// --- Règles spécifiques par événement --- // --- Règles spécifiques par événement ---

View File

@@ -51,4 +51,13 @@ return [
// --- Logs --- // --- Logs ---
'LOG_LEVEL' => 'INFO', // DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL 'LOG_LEVEL' => 'INFO', // DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL
'STATE_DIR' => '/var/lib/monitoring',
'INSTALLED_LOG' => '/var/lib/monitoring/installed-files.log',
'CRON_JOBS' => [
"*/5 * * * * bash {BASE_DIR}/bin/check_disk.sh > /dev/null 2>&1",
"*/15 * * * * bash {BASE_DIR}/bin/check_smart.sh > /dev/null 2>&1",
"10 3 * * * php {BASE_DIR}/bin/monitoring-update.php > /dev/null 2>&1",
"* * * * * php {BASE_DIR}/bin/alert-engine.php > /dev/null 2>&1"
],
]; ];

View File

@@ -25,11 +25,16 @@ $CONFIG = [
'LOG_LEVEL' => 'INFO' 'LOG_LEVEL' => 'INFO'
]; ];
if (file_exists($MONITORING_CONF_DIR . '/monitoring.conf.php')) { // 1. On charge la configuration GLOBALE (La vérité est ici)
$global_conf = include $MONITORING_CONF_DIR . '/monitoring.conf.php'; $global_conf = $MONITORING_CONF_DIR . "/monitoring.local.conf.php";
if (is_array($global_conf)) { if (file_exists($global_conf)) {
$CONFIG = array_merge($CONFIG, $global_conf); $CONFIG = array_replace_recursive($CONFIG, include $global_conf);
} }
// 2. On charge ensuite la config spécifique au script (si besoin de surcharger)
// $specific_conf est défini par le script qui appelle la lib
if (isset($specific_conf) && file_exists($specific_conf)) {
$CONFIG = array_replace_recursive($CONFIG, include $specific_conf);
} }
// Variables d'exécution // Variables d'exécution

View File

@@ -1,9 +1,11 @@
f433b3e2ca25c76cccebf971072255dae64169a8ae162d6baa10776904d733e9 755 bin/alert-engine.sh 5b4ea784d2cbe73f6e829e35f23b0b4dbe12df55cc1abc8eba6602da36c724ef 755 bin/alert-engine.php
7ff2eb1163ca8b9aa3927ac7f0ebbcc1f90c944e51afbc880d57359b83a0c73f 755 bin/check_disk.sh fdcea6720186795538f48c08b99103b320273dbdd0ea5246a2da9d81a1eecc6c 755 bin/check_disk.sh
4fae83b48dc25c5e2a59bba944d8c3f2c6dff89bf2adb932d4dd9201f6305ca4 755 bin/install-monitoring.sh ead10d3be3aac48c6406a734dee1bddf9a8abb1e21de102ce72fa92fdecbaf22 755 bin/check_smart.sh
36528963f2e78a160738a2cf3b8da67b9d12dbe495d9d01ca6c1ba97956288fa 755 bin/monitoring.sh 8f95824b568b5de7dbdc2d6ab87fc6fd8076dcb8ad20de3e72a53391e97f8484 755 bin/install-monitoring.sh
78ccebfd1da7cf885fddb8d5a967c23e379c495d8f43490584ace7133690ec55 755 bin/monitoring-update.sh 97a91b13b0776acb3326010821ffcc163e96a97e3c326ea77f11efdb7baf159a 755 bin/log-cli.php
54eb520360c80b3146c5cdb846330a8743cbeb9fe6de0559357114b92d090c29 755 bin/monitor-update-config.sh 02bd43ed2a9b92acc013274c716e6bc50120a8103ccf3d9c4e6f345a0b22d6a0 755 bin/monitoring.php
83db39c8d0cfd6f6e9d3cc5b961a67db29dc73666304a91e0d4a6d5831c623cb 644 conf/alert-engine.conf 97d407d75a26bd2ebbb86a2e5f8dab8b24639e8a9164f42bd554ba7728ab8cb5 755 bin/monitoring-update-config.php
caaa8f6031d66bc43a897ac2804124ce2050a64523734195d5505ae863836bf4 644 conf/monitoring.conf 910a7c3a4423fb5456233d0c6cbcfc7f511c94947580972c42286762142e5ce6 755 bin/monitoring-update.php
654cd98ecda1c485a0ea1224f160a3c4d7396ab95a491603574e2ad1981fe010 644 lib/monitoring-lib.sh dc70c1184da4aa32eebdeaee57cfed23e91397c94a6243e0ac8664968078f0c7 644 conf/alert-engine.conf.php
324038d28f24f3f4d1f6def73752ff703d4ce8b532a663c6628611923748b1f5 644 conf/monitoring.conf.php
9bb7f5438edc5fb6a5b899ee21be2a5a559eb0697a028a4e991fc82362eaa460 644 lib/monitoring-lib.php