ecriture en PHP
This commit is contained in:
204
servers/linux/monitoring/bin/alert-engine.php
Normal file
204
servers/linux/monitoring/bin/alert-engine.php
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#!/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 (format PHP attendu)
|
||||||
|
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 (entrées expirées)
|
||||||
|
*/
|
||||||
|
function cleanup_dedup_file() {
|
||||||
|
global $DEDUP_FILE, $DEDUP_WINDOW;
|
||||||
|
if (!file_exists($DEDUP_FILE)) return;
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$lines = file($DEDUP_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
$kept = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parts = explode('|', $line);
|
||||||
|
if (count($parts) >= 2 && ($now - (int)$parts[1]) <= $DEDUP_WINDOW) {
|
||||||
|
$kept[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_put_contents($DEDUP_FILE, implode("\n", $kept) . (empty($kept) ? "" : "\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si une alerte doit être envoyée (Déduplication)
|
||||||
|
*/
|
||||||
|
function should_notify_dedup($key) {
|
||||||
|
global $DEDUP_FILE, $DEDUP_WINDOW;
|
||||||
|
if (!file_exists($DEDUP_FILE)) return true;
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$last_ts = 0;
|
||||||
|
|
||||||
|
$handle = fopen($DEDUP_FILE, 'r');
|
||||||
|
while (($line = fgets($handle)) !== false) {
|
||||||
|
$p = explode('|', trim($line));
|
||||||
|
if (count($p) >= 5) {
|
||||||
|
$current_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}";
|
||||||
|
if ($current_key === $key) {
|
||||||
|
$last_ts = (int)$p[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return ($now - $last_ts) >= $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'];
|
||||||
|
|
||||||
|
if (in_array($level, ['DEBUG', 'INFO', 'NOTICE'])) return;
|
||||||
|
if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return;
|
||||||
|
|
||||||
|
// Déduplication
|
||||||
|
$key = "{$data['host']}|{$data['app']}|{$level}|{$event}";
|
||||||
|
if (!should_notify_dedup($key)) {
|
||||||
|
log_debug("alert_suppressed_dedup", "Alerte dédupliquée", ["event=$event", "host={$data['host']}"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détermination des canaux (Règle spécifique puis défaut)
|
||||||
|
$channels_str = $CONFIG['RULES'][$event] ?? $CONFIG['DEFAULT_CHANNELS'][$level] ?? '';
|
||||||
|
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();
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Copyright (C) 2026 Cédric Abonnel
|
# Copyright (C) 2026 Cédric Abonnel
|
||||||
#
|
# License: GNU Affero General Public License v3
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
BASE_DIR="/opt/monitoring"
|
BASE_DIR="/opt/monitoring"
|
||||||
CONF_DIR="${BASE_DIR}/conf"
|
CONF_DIR="${BASE_DIR}/conf"
|
||||||
LOG_DIR="/var/log/monitoring"
|
LOG_DIR="/var/log/monitoring"
|
||||||
@@ -24,11 +16,12 @@ UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/ma
|
|||||||
MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
|
MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
|
||||||
|
|
||||||
INSTALL_DEPS="${INSTALL_DEPS:-true}"
|
INSTALL_DEPS="${INSTALL_DEPS:-true}"
|
||||||
CREATE_LOCAL_CONF="${CREATE_LOCAL_CONF:-true}"
|
|
||||||
|
# --- Fonctions ---
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if [ "${EUID}" -ne 0 ]; then
|
if [ "${EUID}" -ne 0 ]; then
|
||||||
echo "Ce script doit être exécuté en root." >&2
|
echo "ERREUR: Ce script doit être exécuté en root." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -38,35 +31,38 @@ install_deps() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
||||||
apt-get install -y curl coreutils findutils grep sed gawk util-linux ca-certificates
|
apt-get install -y curl coreutils findutils grep sed gawk util-linux ca-certificates
|
||||||
|
# Ajout des modules PHP nécessaires pour vos scripts (curl pour ntfy)
|
||||||
|
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() {
|
||||||
mkdir -p "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}"
|
echo "--- Préparation des répertoires ---"
|
||||||
|
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}"
|
chmod 755 "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_manifest() {
|
fetch_manifest() {
|
||||||
|
echo "--- Récupération du manifeste ---"
|
||||||
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}$/ &&
|
||||||
$2 ~ /^(644|755|600)$/ &&
|
$2 ~ /^(644|755|600)$/ &&
|
||||||
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
|
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
|
||||||
$3 !~ /\.\./
|
$3 !~ /\.\./
|
||||||
' "${TMP_DIR}/manifest.txt" >/dev/null
|
' "${TMP_DIR}/manifest.txt"
|
||||||
}
|
|
||||||
|
|
||||||
apply_mode() {
|
|
||||||
local mode="$1"
|
|
||||||
local file="$2"
|
|
||||||
chmod "$mode" "$file"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
download_one() {
|
download_one() {
|
||||||
@@ -76,87 +72,85 @@ download_one() {
|
|||||||
|
|
||||||
local url="${UPDATE_BASE_URL}/${rel_path}"
|
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 [[ "$rel_path" == conf/* ]] && [ -f "$dst" ]; then
|
||||||
|
echo "Skip: $rel_path (existe déjà)"
|
||||||
|
return 0
|
||||||
|
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")"
|
||||||
|
|
||||||
curl -fsS "$url" -o "$tmp_file"
|
if ! curl -fsS "$url" -o "$tmp_file"; then
|
||||||
|
echo "ERREUR: Échec du téléchargement de ${url}" >&2
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
return 1
|
||||||
|
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 "Hash invalide pour ${rel_path}" >&2
|
echo "ERREUR: Hash invalide pour ${rel_path}" >&2
|
||||||
rm -f "$tmp_file"
|
rm -f "$tmp_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "$dst")"
|
mkdir -p "$(dirname "$dst")"
|
||||||
apply_mode "$mode" "$tmp_file"
|
|
||||||
mv -f "$tmp_file" "$dst"
|
mv -f "$tmp_file" "$dst"
|
||||||
|
chmod "$mode" "$dst"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_from_manifest() {
|
install_from_manifest() {
|
||||||
|
echo "--- Installation des fichiers ---"
|
||||||
while read -r hash mode rel_path; do
|
while read -r hash mode rel_path; do
|
||||||
[ -n "${hash:-}" ] || continue
|
[ -n "${hash:-}" ] || continue
|
||||||
download_one "$hash" "$mode" "$rel_path"
|
download_one "$hash" "$mode" "$rel_path"
|
||||||
done < "${TMP_DIR}/manifest.txt"
|
done < "${TMP_DIR}/manifest-valid.txt"
|
||||||
}
|
|
||||||
|
|
||||||
create_local_conf_if_missing() {
|
|
||||||
if [ "${CREATE_LOCAL_CONF}" != "true" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "${CONF_DIR}/alert-engine.local.conf" ]; then
|
|
||||||
cat > "${CONF_DIR}/alert-engine.local.conf" <<'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
NTFY_SERVER="https://ntfy.sh"
|
|
||||||
NTFY_TOPIC="FjdJ7qex2oGqZkV3OMaqNIxe"
|
|
||||||
NTFY_TOKEN="A_REMPLACER"
|
|
||||||
|
|
||||||
DEST="root"
|
|
||||||
EOF
|
|
||||||
chmod 600 "${CONF_DIR}/alert-engine.local.conf"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_next_steps() {
|
show_next_steps() {
|
||||||
cat <<'EOF'
|
cat <<EOF
|
||||||
|
|
||||||
Installation terminée.
|
Installation terminée avec succès dans ${BASE_DIR}.
|
||||||
|
|
||||||
Étapes suivantes :
|
Étapes suivantes :
|
||||||
1. Éditer /opt/monitoring/conf/alert-engine.local.conf
|
1. Configurez vos alertes :
|
||||||
2. Remplacer NTFY_TOKEN par le vrai token
|
cp ${CONF_DIR}/alert-engine.conf.php ${CONF_DIR}/alert-engine.local.conf.php
|
||||||
3. Tester :
|
nano ${CONF_DIR}/alert-engine.local.conf.php
|
||||||
/opt/monitoring/bin/check_disk.sh
|
|
||||||
/opt/monitoring/bin/alert-engine.sh
|
|
||||||
4. Ajouter cron ou systemd timer
|
|
||||||
|
|
||||||
Exemple cron :
|
2. Initialisez la configuration globale :
|
||||||
*/5 * * * * /opt/monitoring/bin/check_disk.sh
|
cp ${CONF_DIR}/monitoring.conf.php ${CONF_DIR}/monitoring.local.conf.php
|
||||||
*/5 * * * * /opt/monitoring/bin/check_ram.sh
|
|
||||||
15 */6 * * * /opt/monitoring/bin/check_cert.sh
|
3. Lancez un audit des configurations :
|
||||||
30 2 * * * /opt/monitoring/bin/check_backup.sh
|
php ${BASE_DIR}/bin/monitoring-update-config.php
|
||||||
10 3 * * * /opt/monitoring/bin/monitoring-update.sh
|
|
||||||
* * * * * /opt/monitoring/bin/alert-engine.sh
|
4. Planifiez les tâches (cron) :
|
||||||
|
*/5 * * * * php ${BASE_DIR}/bin/alert-engine.php
|
||||||
|
10 3 * * * php ${BASE_DIR}/bin/monitoring-update.php
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
require_root
|
require_root
|
||||||
install_deps
|
install_deps
|
||||||
prepare_dirs
|
prepare_dirs
|
||||||
fetch_manifest
|
fetch_manifest
|
||||||
|
|
||||||
if ! validate_manifest; then
|
if ! validate_manifest > "${TMP_DIR}/manifest-valid.txt"; then
|
||||||
echo "Le manifeste est invalide." >&2
|
echo "ERREUR: Le manifeste est invalide ou corrompu." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install_from_manifest
|
install_from_manifest
|
||||||
create_local_conf_if_missing
|
|
||||||
|
# Nettoyage
|
||||||
|
rm -rf "${TMP_DIR}"
|
||||||
|
|
||||||
show_next_steps
|
show_next_steps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
104
servers/linux/monitoring/bin/monitoring-update-config.php
Normal file
104
servers/linux/monitoring/bin/monitoring-update-config.php
Normal 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();
|
||||||
219
servers/linux/monitoring/bin/monitoring-update.php
Normal file
219
servers/linux/monitoring/bin/monitoring-update.php
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Moteur de mise à jour des programmes
|
||||||
|
* Copyright (C) 2026 Cédric Abonnel
|
||||||
|
* License: GNU Affero General Public License v3
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/monitoring-lib.php';
|
||||||
|
|
||||||
|
// --- Chargement de la configuration spécifique ---
|
||||||
|
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");
|
||||||
|
|
||||||
|
if (!$UPDATE_ENABLED) {
|
||||||
|
log_notice("update_disabled", "Mise à jour désactivée par configuration");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($UPDATE_TMP_DIR)) {
|
||||||
|
mkdir($UPDATE_TMP_DIR, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge et valide le manifeste
|
||||||
|
*/
|
||||||
|
function fetch_manifest() {
|
||||||
|
global $UPDATE_MANIFEST_URL, $UPDATE_TIMEOUT_TOTAL;
|
||||||
|
|
||||||
|
$content = @file_get_contents($UPDATE_MANIFEST_URL, false, stream_context_create([
|
||||||
|
'http' => ['timeout' => $UPDATE_TIMEOUT_TOTAL]
|
||||||
|
]));
|
||||||
|
|
||||||
|
if ($content === false) {
|
||||||
|
log_error("manifest_download_failed", "Impossible de télécharger le manifeste", ["url=$UPDATE_MANIFEST_URL"]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", trim($content));
|
||||||
|
$manifest_data = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (empty(trim($line))) continue;
|
||||||
|
|
||||||
|
$parts = preg_split('/\s+/', trim($line));
|
||||||
|
if (count($parts) !== 3) continue;
|
||||||
|
|
||||||
|
list($hash, $mode, $path) = $parts;
|
||||||
|
|
||||||
|
// Validation stricte (Regex identique au Bash)
|
||||||
|
if (preg_match('/^[0-9a-f]{64}$/i', $hash) &&
|
||||||
|
preg_match('/^(644|755)$/', $mode) &&
|
||||||
|
preg_match('/^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/', $path) &&
|
||||||
|
strpos($path, '..') === false) {
|
||||||
|
$manifest_data[] = ['hash' => strtolower($hash), 'mode' => $mode, 'path' => $path];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())) {
|
||||||
|
log_notice("file_deleted", "Fichier obsolète supprimé", ["file=$rel_path"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lancement du script de synchronisation local
|
||||||
|
*/
|
||||||
|
function run_local_conf_sync() {
|
||||||
|
global $MONITORING_BASE_DIR;
|
||||||
|
$sync_script = $MONITORING_BASE_DIR . '/bin/monitoring-update-config.sh'; // On cherche la version PHP
|
||||||
|
|
||||||
|
if (file_exists($sync_script)) {
|
||||||
|
log_info("local_conf_sync_start", "Synchronisation des configs locales");
|
||||||
|
passthru("php " . escapeshellarg($sync_script), $return_var);
|
||||||
|
if ($return_var !== 0) {
|
||||||
|
log_warning("local_conf_sync_failed", "Échec de synchronisation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Exécution principale ---
|
||||||
|
|
||||||
|
$manifest = fetch_manifest();
|
||||||
|
if (!$manifest) exit(2);
|
||||||
|
|
||||||
|
$total = count($manifest);
|
||||||
|
$updated = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$remote_paths = [];
|
||||||
|
|
||||||
|
foreach ($manifest as $item) {
|
||||||
|
$remote_paths[] = $item['path'];
|
||||||
|
if (update_one_file($item['hash'], $item['mode'], $item['path'])) {
|
||||||
|
$updated++;
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_extra_files($remote_paths);
|
||||||
|
run_local_conf_sync();
|
||||||
|
|
||||||
|
if ($failed > 0) {
|
||||||
|
log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total=$total", "failed=$failed"]);
|
||||||
|
} else {
|
||||||
|
log_info("update_finished", "Mise à jour terminée", ["total=$total", "checked=$updated"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_with_status();
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# 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.
|
||||||
|
#
|
||||||
|
# Moteur de mise à jour des programmes et fichiers connexes
|
||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
|||||||
227
servers/linux/monitoring/bin/monitoring.php
Normal file
227
servers/linux/monitoring/bin/monitoring.php
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Monitoring Update Engine - PHP Version
|
||||||
|
* Copyright (C) 2026 Cédric Abonnel
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../lib/monitoring-lib.php';
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
// On s'appuie sur le chargement de la lib, mais on surcharge si nécessaire
|
||||||
|
$conf_file = "/opt/monitoring/conf/autoupdate.conf.php"; // Format PHP recommandé
|
||||||
|
// --- Chargement de la configuration spécifique ---
|
||||||
|
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;
|
||||||
|
$MONITORING_BASE_DIR = $CONFIG['MONITORING_BASE_DIR'] ?? '/opt/monitoring';
|
||||||
|
|
||||||
|
// --- Initialisation ---
|
||||||
|
lock_or_exit("monitoring-update");
|
||||||
|
|
||||||
|
if (!$UPDATE_ENABLED) {
|
||||||
|
log_notice("update_disabled", "Mise à jour désactivée par configuration");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($UPDATE_TMP_DIR)) {
|
||||||
|
if (!mkdir($UPDATE_TMP_DIR, 0755, true)) {
|
||||||
|
fail_internal("Impossible de créer le répertoire temporaire: $UPDATE_TMP_DIR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge et valide le manifeste
|
||||||
|
*/
|
||||||
|
function fetch_manifest($url) {
|
||||||
|
global $UPDATE_TIMEOUT_TOTAL;
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL);
|
||||||
|
curl_setopt($ch, CURLOPT_FAILONERROR, true);
|
||||||
|
|
||||||
|
$content = curl_exec($ch);
|
||||||
|
if (curl_errno($ch)) {
|
||||||
|
log_error("manifest_download_failed", "Impossible de télécharger le manifeste", ["url" => $url, "error" => curl_error($ch)]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$manifest_entries = [];
|
||||||
|
$lines = explode("\n", trim($content));
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line)) continue;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un fichier spécifique
|
||||||
|
*/
|
||||||
|
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']);
|
||||||
|
|
||||||
|
// Calcul du hash local actuel
|
||||||
|
$local_hash = file_exists($target_file) ? hash_file('sha256', $target_file) : "";
|
||||||
|
|
||||||
|
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();
|
||||||
|
$ch = curl_init($remote_url);
|
||||||
|
$fp = fopen($tmp_file, 'wb');
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL);
|
||||||
|
curl_setopt($ch, CURLOPT_FAILONERROR, true);
|
||||||
|
|
||||||
|
$success = curl_exec($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
if (!$success) {
|
||||||
|
log_error("update_download_failed", "Téléchargement impossible", ["file" => $rel_path, "url" => $remote_url, "error" => $error]);
|
||||||
|
@unlink($tmp_file);
|
||||||
|
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);
|
||||||
|
chmod($tmp_file, octdec($entry['mode']));
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les fichiers locaux absents du manifeste
|
||||||
|
*/
|
||||||
|
function delete_extra_files($remote_files) {
|
||||||
|
global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR;
|
||||||
|
if (!$UPDATE_ALLOW_DELETE) return;
|
||||||
|
|
||||||
|
$directories = ['bin', 'lib', 'conf'];
|
||||||
|
foreach ($directories 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) {
|
||||||
|
// On récupère le chemin relatif par rapport à la racine du monitoring
|
||||||
|
$rel_path = substr($file->getPathname(), strlen($MONITORING_BASE_DIR) + 1);
|
||||||
|
|
||||||
|
// 1. Protection : Si c'est dans le manifeste distant, on ne touche à rien
|
||||||
|
if (in_array($rel_path, $remote_files)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Protection générique : On n'efface JAMAIS les fichiers de configuration locale
|
||||||
|
// Cela couvre : *.local.conf.php, *.local.conf, et même *.local.php par sécurité
|
||||||
|
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 ---
|
||||||
|
|
||||||
|
$manifest = fetch_manifest($UPDATE_MANIFEST_URL);
|
||||||
|
if ($manifest === false) exit(2);
|
||||||
|
|
||||||
|
$total = count($manifest);
|
||||||
|
$updated = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$remote_paths = [];
|
||||||
|
|
||||||
|
foreach ($manifest as $entry) {
|
||||||
|
$remote_paths[] = $entry['path'];
|
||||||
|
if (update_one_file($entry)) {
|
||||||
|
$updated++;
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_extra_files($remote_paths);
|
||||||
|
|
||||||
|
if ($failed > 0) {
|
||||||
|
log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total" => $total, "updated" => $updated, "failed" => $failed]);
|
||||||
|
} else {
|
||||||
|
log_info("update_finished", "Mise à jour terminée", ["total" => $total, "updated" => $updated]);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_with_status();
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# 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.
|
||||||
|
#
|
||||||
|
# lit le fichier log
|
||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
|||||||
71
servers/linux/monitoring/conf/alert-engine.conf.php
Normal file
71
servers/linux/monitoring/conf/alert-engine.conf.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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
|
||||||
|
|
||||||
|
// --- Configuration ntfy ---
|
||||||
|
'NTFY_SERVER' => 'https://ntfy.sh',
|
||||||
|
'NTFY_TOPIC' => 'TPOSOB84sBJ6HTZ7',
|
||||||
|
'NTFY_TOKEN' => '',
|
||||||
|
'NTFY_CLICK_URL' => '',
|
||||||
|
|
||||||
|
// --- 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' => [
|
||||||
|
'WARNING' => 'ntfy',
|
||||||
|
'ERROR' => 'ntfy,mail',
|
||||||
|
'CRITICAL' => 'ntfy,mail',
|
||||||
|
],
|
||||||
|
|
||||||
|
// --- Tags ntfy par niveau ---
|
||||||
|
'NTFY_TAGS' => [
|
||||||
|
'WARNING' => 'warning',
|
||||||
|
'ERROR' => 'warning,rotating_light',
|
||||||
|
'CRITICAL' => 'skull,warning',
|
||||||
|
],
|
||||||
|
|
||||||
|
// --- 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',
|
||||||
|
],
|
||||||
|
];
|
||||||
54
servers/linux/monitoring/conf/monitoring.conf.php
Normal file
54
servers/linux/monitoring/conf/monitoring.conf.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?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
|
||||||
|
];
|
||||||
161
servers/linux/monitoring/lib/monitoring-lib.php
Normal file
161
servers/linux/monitoring/lib/monitoring-lib.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?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'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (file_exists($MONITORING_CONF_DIR . '/monitoring.conf.php')) {
|
||||||
|
$global_conf = include $MONITORING_CONF_DIR . '/monitoring.conf.php';
|
||||||
|
if (is_array($global_conf)) {
|
||||||
|
$CONFIG = array_merge($CONFIG, $global_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user