Compare commits

..

31 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
ae0c8f95cb ecriture en PHP 2026-03-16 22:14:35 +01:00
d31e193954 suppresion de la ligne brute 2026-03-16 20:13:13 +01:00
20 changed files with 1314 additions and 187 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`).
* **Logs JSONL :** Centralisation de tous les événements dans `/var/log/monitoring/events.jsonl` pour une analyse facile.
* **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.
* **Auto-update :** Capacité de se mettre à jour automatiquement depuis un dépôt Git via un manifeste.
* **Robuste :** Gestion des verrous (locks) pour éviter que deux instances d'un même script ne tournent en même temps.
* **Moteur PHP & Sondes Hybrides :** Traitement performant des alertes en PHP, tout en gardant des sondes système simples (Bash ou PHP).
* **Alertes Intelligentes :** Envoi via **ntfy** (avec tags et priorités) ou **email**, incluant un système de **déduplication** pour éviter le spam.
* **Logs JSONL :** Centralisation au format standard `JSON Lines` dans `/var/log/monitoring/events.jsonl` pour une exploitation facile.
* **Configuration en cascade :** Système de fichiers `.local.conf.php` pour protéger vos réglages lors des mises à jour.
* **Auto-update & Audit :** Mise à jour automatique via manifeste et script d'audit pour détecter les nouvelles options de configuration manquantes.
---
## 📦 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
# 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/lib/` : Bibliothèque commune (`monitoring-lib.sh`).
* `/opt/monitoring/conf/` : Fichiers de configuration.
* `/var/log/monitoring/` : Logs des événements.
* `/var/lib/monitoring/` : États (offsets de lecture, déduplication).
* `/opt/monitoring/bin/` : Exécutables (sondes, moteur `alert-engine.php`, updater).
* `/opt/monitoring/lib/` : Bibliothèque partagée (`monitoring-lib.php`).
* `/opt/monitoring/conf/` : Fichiers de configuration PHP.
* `/var/log/monitoring/` : Journal des événements (`events.jsonl`).
* `/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)
Éditez le fichier local (prioritaire sur la config par défaut) :
`nano /opt/monitoring/conf/alert-engine.local.conf`
Ne modifiez pas les fichiers `.conf.php` (risques d'écrasement). Créez vos fichiers locaux :
`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_TOPIC` : Le nom de votre topic.
* `DEST` : L'adresse email de réception.
* `NTFY_TOKEN` & `NTFY_TOPIC`.
* `DEST` (votre 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 :
---
## 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
```bash
php /opt/monitoring/bin/monitoring-update-config.php
```
## 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`
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.
Le système est conçu pour être piloté par `cron`. Voici la configuration recommandée :
| 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.*

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env php
<?php
/**
* Alert Engine - PHP Version
* Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
*/
require_once __DIR__ . '/../lib/monitoring-lib.php';
// --- Initialisation de la configuration spécifique ---
// 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) {
if (file_exists($conf)) {
$extra_conf = include $conf;
if (is_array($extra_conf)) {
$CONFIG = array_replace_recursive($CONFIG, $extra_conf);
}
}
}
// Chemins des fichiers
$LOG_SOURCE = $CONFIG['LOG_FILE'] ?? '/var/log/monitoring/events.jsonl';
$STATE_FILE = $CONFIG['ALERT_STATE_FILE'] ?? '/var/lib/monitoring/alert-engine.offset';
$DEDUP_FILE = $CONFIG['ALERT_DEDUP_FILE'] ?? '/var/lib/monitoring/alert-engine.dedup';
$DEDUP_WINDOW = $CONFIG['ALERT_DEDUP_WINDOW'] ?? 3600;
// Création des répertoires si nécessaire
ensure_parent_dir($STATE_FILE);
ensure_parent_dir($DEDUP_FILE);
/**
* 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() {
global $DEDUP_FILE, $DEDUP_WINDOW;
// Si le fichier n'existe pas, rien à nettoyer
if (!file_exists($DEDUP_FILE)) {
return;
}
$now = time();
$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) {
$parts = explode('|', $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
}
}
}
// 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)
* La clé attendue est : "hostname|app|level|event"
*/
function should_notify_dedup(string $key): bool {
global $DEDUP_FILE, $DEDUP_WINDOW;
if (!file_exists($DEDUP_FILE)) {
return true;
}
$now = time();
$last_ts = 0;
$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) {
$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) {
$row_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}";
if ($row_key === $key) {
$last_ts = (int)$p[1];
}
}
}
fclose($handle);
// 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
*/
function send_ntfy($title, $body, $level) {
global $CONFIG;
if (!($CONFIG['ALERT_NTFY_ENABLED'] ?? true)) return true;
if (empty($CONFIG['NTFY_SERVER']) || empty($CONFIG['NTFY_TOPIC'])) return false;
$priority = ['CRITICAL' => 'urgent', 'ERROR' => 'high', 'WARNING' => 'default'][$level] ?? 'default';
$tags = ($CONFIG['NTFY_TAGS'][$level]) ?? 'warning';
$url = rtrim($CONFIG['NTFY_SERVER'], '/') . '/' . $CONFIG['NTFY_TOPIC'];
$headers = ["Title: $title", "Priority: $priority", "Tags: $tags"];
if (!empty($CONFIG['NTFY_TOKEN'])) {
$headers[] = "Authorization: Bearer " . $CONFIG['NTFY_TOKEN'];
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($status >= 200 && $status < 300);
}
/**
* Envoi par mail
*/
function send_mail($subject, $body) {
global $CONFIG;
if (!($CONFIG['ALERT_MAIL_ENABLED'] ?? true)) return true;
if (empty($CONFIG['DEST'])) return false;
$prefix = $CONFIG['ALERT_MAIL_SUBJECT_PREFIX'] ?? '[monitoring]';
$headers = "From: monitoring@" . gethostname() . "\r\n" . "Content-Type: text/plain; charset=UTF-8";
return mail($CONFIG['DEST'], "$prefix $subject", $body, $headers);
}
/**
* Traitement d'une ligne de log
*/
function process_line($line) {
global $CONFIG, $DEDUP_FILE;
$data = json_decode($line, true);
if (!$data || !isset($data['level'], $data['event'])) return;
$level = strtoupper($data['level']);
$event = $data['event'];
// On garde uniquement l'ignore list explicite pour les événements
if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return;
// Déduplication
$key = "{$data['host']}|{$data['app']}|{$level}|{$event}";
if (!should_notify_dedup($key)) {
return;
}
// Détermination des canaux
$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;
$channels = explode(',', $channels_str);
$title = "{$data['host']} [{$data['app']}] $level $event";
$body = sprintf(
"Date: %s\nHôte: %s\nScript: %s\nNiveau: %s\nÉvénement: %s\n\nMessage:\n%s",
$data['ts'] ?? 'N/A', $data['host'], $data['app'], $level, $event, $data['message'] ?? ''
);
foreach ($channels as $ch) {
$ch = trim($ch);
$success = false;
if ($ch === 'ntfy') {
$success = send_ntfy($title, $body, $level);
$success ? log_info("alert_sent_ntfy", "Notification ntfy envoyée", ["event=$event"])
: log_error("alert_ntfy_failed", "Échec ntfy", ["event=$event"]);
} elseif ($ch === 'mail') {
$success = send_mail($title, $body);
$success ? log_info("alert_sent_mail", "Mail envoyé", ["event=$event"])
: log_error("alert_mail_failed", "Échec mail", ["event=$event"]);
}
}
// Enregistrement déduplication
$entry = sprintf("%s|%s|%s|%s|%s\n", $data['host'], time(), $data['app'], $level, $event);
file_put_contents($DEDUP_FILE, $entry, FILE_APPEND);
}
// --- Main ---
lock_or_exit("alert-engine");
if (!file_exists($LOG_SOURCE)) {
log_notice("alert_log_missing", "Fichier de log absent", ["file=$LOG_SOURCE"]);
exit(0);
}
$last_offset = file_exists($STATE_FILE) ? (int)file_get_contents($STATE_FILE) : 0;
$current_size = filesize($LOG_SOURCE);
// Gestion de la rotation de log
if ($last_offset > $current_size) {
log_notice("alert_offset_reset", "Rotation détectée", ["old=$last_offset", "new=0"]);
$last_offset = 0;
}
cleanup_dedup_file();
$fp = fopen($LOG_SOURCE, 'r');
if ($last_offset > 0) fseek($fp, $last_offset);
while (($line = fgets($fp)) !== false) {
if (trim($line) !== '') {
process_line($line);
}
}
fclose($fp);
file_put_contents($STATE_FILE, $current_size);
exit_with_status();

View File

@@ -13,41 +13,49 @@
set -u
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
# --- Configuration (Seuils par défaut) ---
WARNING=80
CRITICAL=95
MOUNTS=("/" "/var" "/home")
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
for mount in "${MOUNTS[@]}"; do
# On vérifie si le point de montage existe avant de tester
if ! mountpoint -q "$mount"; then
continue
fi
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_error "check_failed" "Impossible de lire l'utilisation disque" "mount=$mount"
continue
$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
level="$(threshold_level "$used_pct" "$WARNING" "$CRITICAL")"
# --- 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}')"
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
if [[ ! "$inode_pct" =~ ^[0-9]+$ ]]; then
$LOG_BIN ERROR "check_failed" "Erreur lecture inodes $mount."
else
if [ "$inode_pct" -ge "$CRITICAL" ]; then
$LOG_BIN CRITICAL "inode_usage_critical" "Inodes $mount critiques ($inode_pct%)."
elif [ "$inode_pct" -ge "$WARNING" ]; then
$LOG_BIN WARNING "inode_usage_high" "Inodes $mount élevés ($inode_pct%)."
else
$LOG_BIN INFO "inode_ok" "Inodes $mount OK ($inode_pct%)."
fi
fi
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

@@ -1,55 +1,57 @@
#!/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.
# License: GNU Affero General Public License v3
set -euo pipefail
# --- Configuration ---
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"
# 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"
MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
INSTALL_DEPS="${INSTALL_DEPS:-true}"
CREATE_LOCAL_CONF="${CREATE_LOCAL_CONF:-true}"
# --- 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() {
if [ "${EUID}" -ne 0 ]; then
echo "Ce script doit être exécuté en root." >&2
err "Ce script doit être exécuté en root."
exit 1
fi
}
install_deps() {
if [ "${INSTALL_DEPS}" != "true" ]; then
return 0
fi
if [ "${INSTALL_DEPS}" != "true" ]; then return 0; fi
info "Vérification des dépendances système..."
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
apt-get update -qq
apt-get install -y -qq curl coreutils findutils grep sed gawk util-linux ca-certificates php-cli php-curl php-common smartmontools > /dev/null
ok "Dépendances installées."
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}"
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}"
touch "$INSTALLED_LOG"
}
fetch_manifest() {
info "Téléchargement du manifeste distant..."
curl -fsS "${MANIFEST_URL}" -o "${TMP_DIR}/manifest.txt"
}
@@ -60,89 +62,86 @@ validate_manifest() {
$2 ~ /^(644|755|600)$/ &&
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
$3 !~ /\.\./
' "${TMP_DIR}/manifest.txt" >/dev/null
' "${TMP_DIR}/manifest.txt"
}
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}"
download_and_install() {
local expected_hash=$1 mode=$2 rel_path=$3
local dst="${BASE_DIR}/${rel_path}"
if [ -f "$dst" ]; then
local current_hash
current_hash=$(sha256sum "$dst" | awk '{print $1}')
[ "$current_hash" == "$expected_hash" ] && return 0
info "Mise à jour : $rel_path"
else
info "Installation : $rel_path"
fi
local tmp_file
tmp_file="$(mktemp "${TMP_DIR}/file.XXXXXX")"
curl -fsS "$url" -o "$tmp_file"
if ! curl -fsS "${UPDATE_BASE_URL}/${rel_path}" -o "$tmp_file"; then
err "Échec du téléchargement pour $rel_path"
rm -f "$tmp_file"
return 1
fi
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
echo "Hash invalide pour ${rel_path}" >&2
err "Hash invalide pour $rel_path"
rm -f "$tmp_file"
return 1
fi
mkdir -p "$(dirname "$dst")"
apply_mode "$mode" "$tmp_file"
mv -f "$tmp_file" "$dst"
chmod "$mode" "$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"
# --- NOUVEAUTÉ : Gestion du journal et purge propre ---
update_installed_log() {
# On sauvegarde la liste des chemins relatifs du manifeste validé dans le journal permanent
awk '{print $3}' "${TMP_DIR}/manifest-valid.txt" > "$INSTALLED_LOG"
ok "Journal des fichiers déployés mis à jour ($INSTALLED_LOG)."
}
create_local_conf_if_missing() {
if [ "${CREATE_LOCAL_CONF}" != "true" ]; then
return 0
purge_obsolete_files() {
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
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
# Mode Journal : On lit l'ancien journal pour voir ce qui doit disparaître
while read -r old_file; do
# Si le fichier du journal n'est plus dans le nouveau manifeste
if ! grep -qw "$old_file" "${TMP_DIR}/manifest-valid.txt"; then
if [ -f "${BASE_DIR}/$old_file" ]; then
# Protection ultime des fichiers .local (au cas où ils auraient été loggués par erreur)
if [[ "$old_file" != *".local."* ]]; then
warn "Suppression du fichier obsolète : $old_file"
rm -f "${BASE_DIR}/$old_file"
fi
fi
fi
done < "$INSTALLED_LOG"
}
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 ---
main() {
require_root
@@ -150,14 +149,39 @@ main() {
prepare_dirs
fetch_manifest
if ! validate_manifest; then
echo "Le manifeste est invalide." >&2
if ! validate_manifest > "${TMP_DIR}/manifest-valid.txt"; then
err "Le manifeste est invalide ou corrompu."
exit 1
fi
install_from_manifest
create_local_conf_if_missing
show_next_steps
echo "--------------------------------------------------"
info "Phase 1 : Installation et mises à jour"
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}"
echo "--------------------------------------------------"
ok "Opération terminée avec succès."
# --- 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 "$@"

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

@@ -0,0 +1,104 @@
#!/usr/bin/env php
<?php
/**
* Audit de dérive des configurations PHP
* Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
*/
// --- Initialisation & Sécurité ---
if (posix_getuid() !== 0) {
fwrite(STDERR, "Ce script doit être exécuté en root.\n");
exit(1);
}
const CONF_DIR = '/opt/monitoring/conf';
/**
* Extrait les clés d'un fichier de configuration PHP (tableau return [])
* sans exécuter le fichier pour éviter les effets de bord, via Regex.
*/
function extract_keys($file) {
$content = file_get_contents($file);
// On cherche les patterns : 'KEY' => ou "KEY" =>
preg_match_all('/[\'"]([A-Z0-9_]+)[\'"]\s*=>/i', $content, $matches);
$keys = $matches[1] ?? [];
sort($keys);
return array_unique($keys);
}
/**
* Logique de logging
*/
function log_audit($level, $event, $msg) {
echo sprintf("[%s] %s: %s\n", strtoupper($level), $event, $msg);
}
function check_config_drift() {
$found_issue = false;
$reviewed_files = 0;
$files_requiring_action = 0;
log_audit('info', 'audit_start', "Début de l'audit des configurations PHP");
// On cherche les fichiers .php qui ne sont pas des .local.php
$base_files = glob(CONF_DIR . '/*.php');
$base_files = array_filter($base_files, function($f) {
return !str_ends_with($f, '.local.php');
});
foreach ($base_files as $base_conf) {
$reviewed_files++;
$base_name = basename($base_conf);
$local_conf = str_replace('.php', '.local.php', $base_conf);
$local_name = basename($local_conf);
// 1. Si le fichier local n'existe pas
if (!file_exists($local_conf)) {
if (copy($base_conf, $local_conf)) {
chmod($local_conf, 0600);
log_audit('notice', 'audit_missing_local', "Le fichier $local_name a été créé par copie de $base_name");
} else {
log_audit('error', 'audit_create_local_failed', "Impossible de créer $local_name");
$found_issue = true;
$files_requiring_action++;
}
continue;
}
// 2. Comparaison des clés
$keys_base = extract_keys($base_conf);
$keys_local = extract_keys($local_conf);
$missing = array_diff($keys_base, $keys_local); // Présent dans base mais pas local
$obsolete = array_diff($keys_local, $keys_base); // Présent dans local mais plus dans base
if (!empty($missing) || !empty($obsolete)) {
$found_issue = true;
$files_requiring_action++;
log_audit('warning', 'audit_file_requires_action', "Vérification requise pour $local_name");
if (!empty($missing)) {
log_audit('warning', 'audit_keys_missing', "Options absentes de $local_name : " . implode(', ', $missing));
}
if (!empty($obsolete)) {
log_audit('info', 'audit_keys_obsolete', "Options obsolètes ou personnalisées dans $local_name : " . implode(', ', $obsolete));
}
} else {
log_audit('info', 'audit_file_ok', "Le fichier $local_name est synchronisé avec $base_name");
}
}
// Résumé final
if (!$found_issue) {
log_audit('info', 'audit_success', "Toutes les configurations sont à jour ($reviewed_files fichiers vérifiés)");
} else {
log_audit('warning', 'audit_requires_action', "Action requise sur $files_requiring_action fichier(s) sur $reviewed_files");
}
}
// --- Main ---
check_config_drift();

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env php
<?php
/**
* Moteur de mise à jour
* Pilotage du script Bash + Initialisation des Configs + Cron + Ménage
* Copyright (C) 2026 Cédric Abonnel
*/
require_once __DIR__ . '/../lib/monitoring-lib.php';
// Sécurité : Un seul update à la fois
lock_or_exit("monitoring-update");
echo "\e[1m--- Début de la mise à jour système ---\e[0m\n";
// 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);
}
// 2. Exécution du moteur de synchronisation Bash
$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";
/**
* Nettoyage et Mise à jour du Crontab
* @param array $deleted_files Liste des chemins de fichiers supprimés du déploiement
*/
function ensure_crontab_entries($deleted_files) {
global $CONFIG, $MONITORING_BASE_DIR;
// --- CONFIGURATION DU MÉNAGE STATIQUE ---
// 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
];
// Préparation des jobs requis
$required_jobs = array_map(function($job) use ($MONITORING_BASE_DIR) {
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"
]);
$current_cron = shell_exec("crontab -l 2>/dev/null") ?: "";
$lines = explode("\n", trim($current_cron));
$new_lines = [];
$has_changed = false;
// --- PHASE A : Nettoyage (Dynamique + Statique) ---
foreach ($lines as $line) {
$trim_line = trim($line);
if (empty($trim_line)) continue;
$keep = true;
// 1. Nettoyage Dynamique (basé sur le log Git)
foreach ($deleted_files as $deleted_path) {
if (strpos($trim_line, $deleted_path) !== false) {
echo "\e[33m[CLEAN]\e[0m Script supprimé du déploiement : " . basename($deleted_path) . "\n";
$keep = false;
$has_changed = true;
break;
}
}
// 2. Nettoyage Statique (basé sur ta liste forcée)
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 ---
foreach ($required_jobs as $job) {
// Extraction du chemin du script pour éviter les doublons
preg_match('/\/bin\/([a-z0-9_-]+\.(php|sh))/i', $job, $matches);
$script_path = $matches[0] ?? "";
$found = false;
foreach ($new_lines as $line) {
if (strpos($line, $script_path) !== false) {
$found = true;
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";
}
}
// --- PHASE C : Application ---
if ($has_changed) {
$content = implode("\n", array_filter($new_lines, 'trim')) . "\n";
$tmp_cron = tempnam(sys_get_temp_dir(), 'cron');
file_put_contents($tmp_cron, $content);
exec("crontab " . escapeshellarg($tmp_cron));
unlink($tmp_cron);
echo "\e[32m[OK]\e[0m Crontab synchronisé.\n";
} else {
echo "[INFO] Crontab déjà à jour.\n";
}
}
function ensure_local_configs() {
global $MONITORING_CONF_DIR;
$configs = [
'monitoring.conf.php' => 'monitoring.local.conf.php',
'alert-engine.conf.php' => 'alert-engine.conf.local.php',
'autoupdate.conf.php' => 'autoupdate.local.conf.php'
];
foreach ($configs as $src => $dst) {
$dst_path = $MONITORING_CONF_DIR . '/' . $dst;
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);
}
}
}
}

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env php
<?php
/**
* Monitoring Update Engine - PHP Version
* Copyright (C) 2026 Cédric Abonnel
*/
require_once __DIR__ . '/../lib/monitoring-lib.php';
// --- Configuration ---
// Note : La lib a déjà chargé $CONFIG['UPDATE_BASE_URL'] etc. depuis monitoring.local.conf.php
// On ne charge les fichiers spécifiques que s'ils apportent des règles de mise à jour uniques.
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 (fallback si absent de la config globale)
$UPDATE_ENABLED = $CONFIG['UPDATE_ENABLED'] ?? true;
$UPDATE_TMP_DIR = $CONFIG['UPDATE_TMP_DIR'] ?? '/tmp/monitoring-update';
$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;
$MONITORING_BASE_DIR = $MONITORING_BASE_DIR; // Provient de la lib
// --- Initialisation ---
lock_or_exit("monitoring-update");
if (!$UPDATE_ENABLED) {
log_notice("update_disabled", "Mise à jour désactivée");
exit(0);
}
if (!is_dir($UPDATE_TMP_DIR)) {
mkdir($UPDATE_TMP_DIR, 0755, true);
}
/**
* Télécharge et valide le manifeste
*/
function fetch_manifest($url) {
global $UPDATE_TIMEOUT_TOTAL;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $UPDATE_TIMEOUT_TOTAL,
CURLOPT_FAILONERROR => true,
CURLOPT_FOLLOWLOCATION => true
]);
$content = curl_exec($ch);
if (curl_errno($ch)) {
log_error("manifest_download_failed", "Échec téléchargement manifeste", ["url" => $url, "err" => curl_error($ch)]);
return false;
}
curl_close($ch);
$manifest_entries = [];
$lines = explode("\n", trim($content));
foreach ($lines as $line) {
if (preg_match('/^([0-9a-fA-F]{64})\s+(644|755)\s+((bin|lib|conf)\/[A-Za-z0-9._\/-]+)$/', trim($line), $matches)) {
$manifest_entries[] = ['hash' => $matches[1], 'mode' => $matches[2], 'path' => $matches[3]];
}
}
return $manifest_entries;
}
/**
* Met à jour un fichier
*/
function update_one_file($entry) {
global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL;
$rel_path = $entry['path'];
$target_file = $MONITORING_BASE_DIR . '/' . $rel_path;
$remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path;
$expected_hash = strtolower($entry['hash']);
if (file_exists($target_file) && hash_file('sha256', $target_file) === $expected_hash) {
return true;
}
$tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . uniqid();
$ch = curl_init($remote_url);
$fp = fopen($tmp_file, 'wb');
curl_setopt_array($ch, [
CURLOPT_FILE => $fp,
CURLOPT_TIMEOUT => $UPDATE_TIMEOUT_TOTAL,
CURLOPT_FAILONERROR => true,
CURLOPT_FOLLOWLOCATION => true
]);
$success = curl_exec($ch);
curl_close($ch);
fclose($fp);
if (!$success || hash_file('sha256', $tmp_file) !== $expected_hash) {
log_error("update_failed", "Fichier invalide ou corrompu", ["file" => $rel_path]);
@unlink($tmp_file);
return false;
}
ensure_parent_dir($target_file);
chmod($tmp_file, octdec($entry['mode']));
safe_mv($tmp_file, $target_file); // Utilise la fonction safe_mv de ta lib
log_notice("file_updated", "Mise à jour appliquée", ["file" => $rel_path]);
return true;
}
/**
* Nettoyage
*/
function delete_extra_files($remote_files) {
global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR, $SCRIPT_PATH;
if (!$UPDATE_ALLOW_DELETE) return;
foreach (['bin', 'lib', 'conf'] as $dir) {
$full_path = $MONITORING_BASE_DIR . '/' . $dir;
if (!is_dir($full_path)) continue;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($iterator as $file) {
$path = $file->getPathname();
$rel_path = substr($path, strlen($MONITORING_BASE_DIR) + 1);
// PROTECTIONS
if (in_array($rel_path, $remote_files)) continue;
if (str_contains($rel_path, '.local.')) continue; // Protection fichiers locaux
if ($path === $SCRIPT_PATH) continue; // Ne pas se suicider
if (@unlink($path)) {
log_notice("file_deleted", "Fichier obsolète supprimé", ["file" => $rel_path]);
}
}
}
}
// --- Main ---
$manifest = fetch_manifest($UPDATE_MANIFEST_URL);
if (!$manifest) exit(2);
$remote_paths = [];
$updated = 0; $failed = 0;
foreach ($manifest as $entry) {
$remote_paths[] = $entry['path'];
update_one_file($entry) ? $updated++ : $failed++;
}
delete_extra_files($remote_paths);
if ($failed > 0) {
log_warning("update_partial", "Mise à jour terminée avec erreurs", ["failed" => $failed]);
} else {
log_info("update_ok", "Mise à jour terminée avec succès");
}
exit_with_status();

View File

@@ -0,0 +1,74 @@
<?php
/**
* Configuration Alert Engine
* Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
* NE PAS ÉDITER - Utilisez alert-engine.local.conf.php pour vos surcharges
* RISQUE D'ECRASEMENT - RISQUE D'EFFACEMENT
*/
// On définit les variables de base (équivalent de l'environnement)
$monitoring_state_dir = getenv('MONITORING_STATE_DIR') ?: '/var/lib/monitoring';
return [
// --- Fichiers d'état ---
'ALERT_STATE_FILE' => $monitoring_state_dir . '/alert-engine.offset',
'ALERT_DEDUP_FILE' => $monitoring_state_dir . '/alert-engine.dedup',
// --- Activation des canaux ---
'ALERT_NTFY_ENABLED' => true,
'ALERT_MAIL_ENABLED' => true,
// --- Configuration Mail ---
'ALERT_MAIL_BIN' => '/usr/sbin/sendmail',
'ALERT_MAIL_SUBJECT_PREFIX' => '[monitoring]',
'DEST' => 'admin@example.com', // N'oubliez pas de définir le destinataire
// --- Déduplication ---
'ALERT_DEDUP_WINDOW' => 3600, // en secondes
// --- Événements à ignorer ---
'ALERT_IGNORE_EVENTS' => [
'update_not_needed',
'alert_sent_ntfy',
'alert_sent_mail'
],
// --- Canaux par défaut selon le niveau ---
'DEFAULT_CHANNELS' => [
'INFO' => 'ntfy',
'NOTICE' => 'ntfy',
'WARNING' => 'ntfy',
'ERROR' => 'ntfy,mail',
'CRITICAL' => 'ntfy,mail',
],
// --- Tags : Une icône pour chaque état possible ---
'NTFY_TAGS' => [
'DEBUG' => 'gear', // ⚙️
'INFO' => 'information_source', //
'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 ---
// Si un événement est listé ici, il outrepasse les DEFAULT_CHANNELS
'RULES' => [
'disk_usage_high' => 'ntfy',
'disk_usage_critical' => 'ntfy,mail',
'check_failed' => 'ntfy,mail',
'internal_error' => 'ntfy,mail',
'update_hash_unavailable' => 'ntfy',
'update_download_failed' => 'ntfy,mail',
'update_hash_mismatch' => 'ntfy,mail',
'manifest_download_failed' => 'ntfy,mail',
'manifest_invalid' => 'ntfy,mail',
'update_finished_with_errors' => 'ntfy,mail',
],
];

View File

@@ -0,0 +1,63 @@
<?php
/**
* Configuration globale par défaut
* Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
* NE PAS ÉDITER - Utilisez monitoring.local.conf.php pour vos surcharges
* RISQUE D'ECRASEMENT - RISQUE D'EFFACEMENT
*/
// Détection dynamique du hostname (équivalent de $(hostname -f))
$hostname_fqdn = getenv('HOSTNAME_FQDN') ?: (gethostname() ?: 'localhost');
// On définit les répertoires de base
$monitoring_base = '/opt/monitoring';
$monitoring_log_dir = '/var/log/monitoring';
$monitoring_state_dir = '/var/lib/monitoring';
$monitoring_lock_dir = '/var/lock/monitoring';
// Initialisation automatique des répertoires (équivalent du mkdir -p à la fin du bash)
foreach ([$monitoring_log_dir, $monitoring_state_dir, $monitoring_lock_dir] as $dir) {
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
}
return [
// --- Chemins ---
'MONITORING_BASE' => $monitoring_base,
'MONITORING_LOG_DIR' => $monitoring_log_dir,
'MONITORING_STATE_DIR' => $monitoring_state_dir,
'MONITORING_LOCK_DIR' => $monitoring_lock_dir,
'LOG_FILE' => $monitoring_log_dir . '/events.jsonl',
// --- Identification ---
'HOSTNAME_FQDN' => $hostname_fqdn,
'DEST' => 'root',
// --- ntfy ---
'NTFY_SERVER' => 'https://ntfy.sh', // Correction du nfy.sh en ntfy.sh
'NTFY_TOPIC' => 'TPOSOB84sBJ6HTZ7',
'NTFY_TOKEN' => '',
// --- Mises à jour (Update) ---
'UPDATE_ENABLED' => true,
'UPDATE_BASE_URL' => 'https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring',
'UPDATE_MANIFEST_URL' => 'https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring/manifest.txt',
'UPDATE_TIMEOUT_CONNECT' => 3,
'UPDATE_TIMEOUT_TOTAL' => 15,
'UPDATE_TMP_DIR' => '/tmp/monitoring-update',
'UPDATE_ALLOW_DELETE' => false,
// --- Logs ---
'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

@@ -0,0 +1,166 @@
<?php
/**
* Monitoring Library - PHP Version
* Copyright (C) 2026 Cédric Abonnel
* License: GNU Affero General Public License v3
*/
// --- Chemins et Constantes ---
$MONITORING_LIB_DIR = __DIR__;
$MONITORING_BASE_DIR = dirname($MONITORING_LIB_DIR);
$MONITORING_CONF_DIR = $MONITORING_BASE_DIR . '/conf';
// États globaux
$STATUS_OK = 0;
$STATUS_WARNING = 1;
$STATUS_ERROR = 2;
$STATUS_INTERNAL = 3;
$CURRENT_STATUS = $STATUS_OK;
// --- Chargement de la Config ---
$CONFIG = [
'LOG_FILE' => '/var/log/monitoring/events.jsonl',
'MONITORING_LOCK_DIR' => '/var/lock/monitoring',
'LOG_LEVEL' => 'INFO'
];
// 1. On charge la configuration GLOBALE (La vérité est ici)
$global_conf = $MONITORING_CONF_DIR . "/monitoring.local.conf.php";
if (file_exists($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
$SCRIPT_NAME = basename($_SERVER['SCRIPT_FILENAME'] ?? $argv[0]);
$SCRIPT_PATH = realpath($_SERVER['SCRIPT_FILENAME'] ?? $argv[0]);
// --- Fonctions de Log ---
/**
* Log un événement au format JSONL
*/
function log_event(string $level, string $event, string $message, array $extra_kv = []) {
global $CONFIG, $SCRIPT_NAME;
$ts = date('c'); // ISO-8601
// Détection Hostname
$host = getenv('HOSTNAME_FQDN') ?: (gethostname() ?: 'unknown');
$log_data = [
"ts" => $ts,
"host" => $host,
"app" => $SCRIPT_NAME,
"level" => $level,
"event" => $event,
"message" => $message
];
// Fusion des paires clé=valeur supplémentaires
foreach ($extra_kv as $kv) {
if (strpos($kv, '=') !== false) {
list($k, $v) = explode('=', $kv, 2);
$log_data[$k] = $v;
}
}
$json_line = json_encode($log_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$log_file = $CONFIG['LOG_FILE'];
ensure_parent_dir($log_file);
file_put_contents($log_file, $json_line . "\n", FILE_APPEND | LOCK_EX);
}
function set_status(int $new_status) {
global $CURRENT_STATUS;
if ($new_status > $CURRENT_STATUS) {
$CURRENT_STATUS = $new_status;
}
}
// Helpers de logs
function log_debug($e, $m, $x = []) { log_event("DEBUG", $e, $m, $x); }
function log_info($e, $m, $x = []) { log_event("INFO", $e, $m, $x); }
function log_notice($e, $m, $x = []) { log_event("NOTICE", $e, $m, $x); }
function log_warning($e, $m, $x = []) { log_event("WARNING", $e, $m, $x); set_status(1); }
function log_error($e, $m, $x = []) { log_event("ERROR", $e, $m, $x); set_status(2); }
function log_critical($e, $m, $x = []) { log_event("CRITICAL", $e, $m, $x); set_status(2); }
function fail_internal(string $msg) {
log_event("ERROR", "internal_error", $msg);
exit(3); // STATUS_INTERNAL
}
function exit_with_status() {
global $CURRENT_STATUS;
exit($CURRENT_STATUS);
}
// --- Utilitaires Système ---
/**
* Vérifie la présence de commandes système
*/
function require_cmd(...$cmds) {
foreach ($cmds as $cmd) {
$output = [];
$res = 0;
exec("command -v " . escapeshellarg($cmd), $output, $res);
if ($res !== 0) {
fail_internal("Commande requise absente: $cmd");
}
}
}
/**
* Gestion du verrouillage (Lock)
*/
function lock_or_exit(?string $lock_name = null) {
global $CONFIG, $SCRIPT_NAME;
$name = $lock_name ?: $SCRIPT_NAME;
$lock_file = ($CONFIG['MONITORING_LOCK_DIR'] ?? '/var/lock/monitoring') . "/{$name}.lock";
ensure_parent_dir($lock_file);
$fp = fopen($lock_file, "w+");
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
log_notice("already_running", "Une autre instance est déjà en cours", ["lock=$lock_file"]);
exit(0);
}
// On garde le descripteur ouvert pour maintenir le lock
return $fp;
}
/**
* Évalue un niveau selon des seuils
*/
function threshold_level($value, $warning, $critical) {
if ($value >= $critical) return 'CRITICAL';
if ($value >= $warning) return 'WARNING';
return 'INFO';
}
function ensure_parent_dir(string $file) {
$dir = dirname($file);
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) {
// Ici on ne peut pas appeler log_event si c'est le répertoire de log qui échoue
error_log("Impossible de créer le répertoire : $dir");
exit(3);
}
}
}
function safe_mv(string $src, string $dst) {
if (!rename($src, $dst)) {
fail_internal("Échec du déplacement de $src vers $dst");
}
}

View File

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

View File

@@ -250,8 +250,6 @@ Niveau: $level
Message:
$message
Ligne brute:
$line
EOF
}

View File

@@ -10,6 +10,8 @@
# 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

View File

@@ -10,6 +10,8 @@
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# lit le fichier log
set -u