Compare commits
4 Commits
e6263c1ae2
...
2c6ecd80a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c6ecd80a7 | |||
| a646c2f4be | |||
| 479fe9c1f1 | |||
| 2b8690c34b |
+9
-9
@@ -178,10 +178,10 @@ process_deployment_files "$destination_dir" "$source_dir" "$file_local_desc"
|
|||||||
## Declarations ##
|
## Declarations ##
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire destination
|
# Spécifier le chemin du répertoire destination
|
||||||
destination_dir="$HOME/.local/share/doc"
|
destination_dir="$HOME/.local/share/doc/scripts-bash"
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire du dépôt Git local
|
# Spécifier le chemin du répertoire du dépôt Git local
|
||||||
source_dir="local/share/doc"
|
source_dir="local/share/doc/scripts-bash"
|
||||||
|
|
||||||
file_local_desc=.config/files_local-share-doc
|
file_local_desc=.config/files_local-share-doc
|
||||||
|
|
||||||
@@ -198,10 +198,10 @@ process_deployment_files "$destination_dir" "$source_dir" "$file_local_desc"
|
|||||||
## Declarations ##
|
## Declarations ##
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire
|
# Spécifier le chemin du répertoire
|
||||||
destination_dir="$HOME/.local/share/man"
|
destination_dir="$HOME/.local/share/man/man1"
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire du dépôt Git local
|
# Spécifier le chemin du répertoire du dépôt Git local
|
||||||
source_dir="local/share/man"
|
source_dir="local/share/man/man1"
|
||||||
|
|
||||||
file_local_desc=.config/files_local-share-man
|
file_local_desc=.config/files_local-share-man
|
||||||
|
|
||||||
@@ -209,12 +209,12 @@ file_local_desc=.config/files_local-share-man
|
|||||||
log "## Debut du traitement pour $destination_dir"
|
log "## Debut du traitement pour $destination_dir"
|
||||||
|
|
||||||
log "### Debut du Traitement pour la gestion du PATH avec $destination_dir"
|
log "### Debut du Traitement pour la gestion du PATH avec $destination_dir"
|
||||||
# Vérifier si le répertoire destination est déjà dans le PATH
|
# man cherche dans $MANPATH/man1/, on ajoute le parent ~/.local/share/man
|
||||||
if [[ ! ":$(manpath):" == *":$destination_dir:"* ]]; then
|
manpath_parent="$HOME/.local/share/man"
|
||||||
# Ajouter le répertoire destination au PATH dans le fichier de configuration de session de l'utilisateur
|
if [[ ! ":$(manpath):" == *":$manpath_parent:"* ]]; then
|
||||||
echo 'export MANPATH="$(manpath):'"$destination_dir"'"' >> ~/.bashrc
|
echo 'export MANPATH="$(manpath):'"$manpath_parent"'"' >> ~/.bashrc
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
log " $destination_dir a été ajouté au MANPATH dans le fichier de configuration de session de l'utilisateur."
|
log " $manpath_parent a été ajouté au MANPATH dans le fichier de configuration de session de l'utilisateur."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
process_deployment_files "$destination_dir" "$source_dir" "$file_local_desc"
|
process_deployment_files "$destination_dir" "$source_dir" "$file_local_desc"
|
||||||
|
|||||||
+39
-29
@@ -1,50 +1,60 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# 1. Liste des hosts
|
mapfile -t ALL_HOSTS < <(
|
||||||
mapfile -t ALL_HOSTS < <(grep -riI "^Host " ~/.ssh/config ~/.ssh/config.d/ ~/.ssh/include/ 2>/dev/null | awk '{print $2}' | grep -v '*' | sort -u)
|
grep -hriI "^Host " ~/.ssh/config ~/.ssh/config.d/ ~/.ssh/include/ 2>/dev/null \
|
||||||
|
| awk '{print $2}' \
|
||||||
|
| grep -v '\*' \
|
||||||
|
| sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
TOTAL=${#ALL_HOSTS[@]}
|
||||||
|
|
||||||
check_host() {
|
check_host() {
|
||||||
local host=$1
|
local host=$1
|
||||||
local info=$(ssh -G "$host")
|
local info addr port user
|
||||||
local addr=$(echo "$info" | awk '/^hostname / {print $2}')
|
info=$(ssh -G "$host" 2>/dev/null)
|
||||||
local port=$(echo "$info" | awk '/^port / {print $2}')
|
addr=$(awk '/^hostname / {print $2}' <<< "$info")
|
||||||
|
port=$(awk '/^port / {print $2}' <<< "$info")
|
||||||
|
user=$(awk '/^user / {print $2}' <<< "$info")
|
||||||
|
|
||||||
# Test de port TCP
|
|
||||||
if (timeout 0.7s bash -c "cat < /dev/null > /dev/tcp/$addr/$port") 2>/dev/null; then
|
if (timeout 0.7s bash -c "cat < /dev/null > /dev/tcp/$addr/$port") 2>/dev/null; then
|
||||||
# On utilise "|" comme délimiteur interne
|
printf "\033[32m[ ON ]\033[0m|ON|%-22s|%s@%s\n" "$host" "$user" "$addr"
|
||||||
printf "[ ON ]|%s\n" "$host"
|
|
||||||
else
|
else
|
||||||
printf "[ OFF ]|%s\n" "$host"
|
printf "\033[31m[ OFF ]\033[0m|OFF|%-22s|%s@%s\n" "$host" "$user" "$addr"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
export -f check_host
|
export -f check_host
|
||||||
echo "Vérification des serveurs..."
|
|
||||||
|
|
||||||
# 2. Scan parallèle
|
printf "Vérification de %d serveurs...\n" "$TOTAL"
|
||||||
STATE_LIST=$(printf "%s\n" "${ALL_HOSTS[@]}" | xargs -I {} -P 10 bash -c 'check_host "{}"')
|
|
||||||
|
|
||||||
# 3. Interface FZF
|
# Scan parallèle — ON affiché en premier, puis tri alphabétique
|
||||||
# On demande à fzf d'afficher les colonnes proprement
|
STATE_LIST=$(
|
||||||
choice=$(echo "$STATE_LIST" | fzf --height 40% --reverse \
|
printf "%s\n" "${ALL_HOSTS[@]}" \
|
||||||
--delimiter="\|" \
|
| xargs -I {} -P 20 bash -c 'check_host "{}"' \
|
||||||
--with-nth=1,2 \
|
| sort -t'|' -k2,2r -k3,3
|
||||||
--header "Tapez 'OFF' pour les serveurs HS | 'ON' pour les actifs")
|
)
|
||||||
|
|
||||||
|
choice=$(echo "$STATE_LIST" | fzf \
|
||||||
|
--ansi \
|
||||||
|
--height 70% \
|
||||||
|
--reverse \
|
||||||
|
--delimiter="|" \
|
||||||
|
--with-nth=1,3,4 \
|
||||||
|
--header "Entrée: connexion | ESC: annuler | Filtre: ON / OFF / nom" \
|
||||||
|
--preview 'host=$(echo {} | cut -d"|" -f3 | tr -d " "); ssh -G "$host" 2>/dev/null | grep -E "^(hostname|user|port|identityfile|compression) " | column -t' \
|
||||||
|
--preview-window=right:45%:wrap)
|
||||||
|
|
||||||
# 4. Connexion propre
|
|
||||||
if [ -n "$choice" ]; then
|
if [ -n "$choice" ]; then
|
||||||
# On extrait le nom de l'hôte en utilisant le délimiteur "|"
|
status=$(echo "$choice" | cut -d'|' -f2)
|
||||||
host_to_connect=$(echo "$choice" | cut -d'|' -f2)
|
host_to_connect=$(echo "$choice" | cut -d'|' -f3 | tr -d '[:space:]')
|
||||||
|
|
||||||
# Nettoyage radical des caractères invisibles ou espaces restants
|
if [[ "$status" == "ON" ]]; then
|
||||||
host_to_connect=$(echo "$host_to_connect" | tr -d '[:space:]')
|
|
||||||
|
|
||||||
if [[ "$choice" == *"[ ON ]"* ]]; then
|
|
||||||
clear
|
clear
|
||||||
ssh "$host_to_connect"
|
exec ssh "$host_to_connect"
|
||||||
else
|
else
|
||||||
echo -e "\033[0;31m⚠️ Le serveur $host_to_connect semble OFFLINE.\033[0m"
|
printf "\033[0;31m⚠ Le serveur '%s' semble OFFLINE.\033[0m\n" "$host_to_connect"
|
||||||
read -p "Tenter quand même la connexion ? (y/n) " confirm
|
read -rp "Tenter quand même la connexion ? (y/n) : " confirm
|
||||||
[[ $confirm == [yY] ]] && ssh "$host_to_connect"
|
[[ $confirm == [yY] ]] && exec ssh "$host_to_connect"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+130
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Audit des fichiers .desktop : chemins manquants, apps cachées, erreurs de syntaxe
|
||||||
|
|
||||||
|
DIRS=(
|
||||||
|
"$HOME/.local/share/applications"
|
||||||
|
"/usr/share/applications"
|
||||||
|
)
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YEL='\033[0;33m'
|
||||||
|
GRN='\033[0;32m'
|
||||||
|
BLU='\033[0;34m'
|
||||||
|
RST='\033[0m'
|
||||||
|
|
||||||
|
ok=0
|
||||||
|
warn=0
|
||||||
|
err=0
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
local f="$1"
|
||||||
|
local name
|
||||||
|
name=$(basename "$f")
|
||||||
|
local issues=()
|
||||||
|
local hints=()
|
||||||
|
|
||||||
|
# Ignorer les fichiers qui ne sont pas des Application affichables
|
||||||
|
local type
|
||||||
|
type=$(grep -m1 "^Type=" "$f" 2>/dev/null | cut -d= -f2-)
|
||||||
|
[[ "$type" != "Application" ]] && return
|
||||||
|
|
||||||
|
# Détecter si c'est un gestionnaire d'URL ou un fichier système (NoDisplay=true y est normal)
|
||||||
|
local is_url_handler=false
|
||||||
|
grep -q "^MimeType=.*x-scheme-handler" "$f" 2>/dev/null && is_url_handler=true
|
||||||
|
|
||||||
|
local is_user_dir=false
|
||||||
|
[[ "$f" == "$HOME/.local/share/applications/"* ]] && is_user_dir=true
|
||||||
|
|
||||||
|
# NoDisplay ou Hidden : signaler uniquement dans le dossier utilisateur et si pas gestionnaire d'URL
|
||||||
|
local nodisplay hidden
|
||||||
|
nodisplay=$(grep -m1 "^NoDisplay=" "$f" | cut -d= -f2-)
|
||||||
|
hidden=$(grep -m1 "^Hidden=" "$f" | cut -d= -f2-)
|
||||||
|
if [[ "$is_user_dir" == true && "$is_url_handler" == false ]]; then
|
||||||
|
[[ "$nodisplay" == "true" ]] && issues+=("caché (NoDisplay=true) — n'apparaît pas dans le menu")
|
||||||
|
[[ "$hidden" == "true" ]] && issues+=("caché (Hidden=true) — n'apparaît pas dans le menu")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exec : extraire le premier token (sans %F %u etc.)
|
||||||
|
local exec_line exec_bin
|
||||||
|
exec_line=$(grep -m1 "^Exec=" "$f" | cut -d= -f2-)
|
||||||
|
# Retirer les guillemets englobants éventuels et les arguments %x
|
||||||
|
exec_bin=$(echo "$exec_line" | awk '{print $1}' | tr -d '"' | sed 's/%[a-zA-Z]$//')
|
||||||
|
|
||||||
|
if [[ -z "$exec_bin" ]]; then
|
||||||
|
issues+=("Exec manquant")
|
||||||
|
elif [[ "$exec_bin" == /* ]]; then
|
||||||
|
# Chemin absolu
|
||||||
|
if [[ ! -f "$exec_bin" ]]; then
|
||||||
|
issues+=("exécutable introuvable : $exec_bin")
|
||||||
|
elif [[ ! -x "$exec_bin" ]]; then
|
||||||
|
issues+=("exécutable non exécutable : $exec_bin")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Commande dans le PATH (ignorer les wrappers shell)
|
||||||
|
if [[ "$exec_bin" != "sh" && "$exec_bin" != "bash" && "$exec_bin" != "env" ]]; then
|
||||||
|
if ! command -v "$exec_bin" &>/dev/null; then
|
||||||
|
issues+=("commande introuvable dans PATH : $exec_bin")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Icône (chemin absolu uniquement — les noms de thème sont difficiles à vérifier)
|
||||||
|
local icon
|
||||||
|
icon=$(grep -m1 "^Icon=" "$f" | cut -d= -f2-)
|
||||||
|
if [[ "$icon" == /* && ! -f "$icon" ]]; then
|
||||||
|
hints+=("icône introuvable : $icon")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validation syntaxique (desktop-file-validate)
|
||||||
|
if command -v desktop-file-validate &>/dev/null; then
|
||||||
|
local syntax_out
|
||||||
|
syntax_out=$(desktop-file-validate "$f" 2>&1 | grep -v "^$")
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
if echo "$line" | grep -q "error:"; then
|
||||||
|
issues+=("syntaxe : ${line#*error: }")
|
||||||
|
elif echo "$line" | grep -q "warning:"; then
|
||||||
|
hints+=("syntaxe : ${line#*warning: }")
|
||||||
|
fi
|
||||||
|
done <<< "$syntax_out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Affichage
|
||||||
|
local max_hints=3
|
||||||
|
if [[ ${#issues[@]} -gt 0 ]]; then
|
||||||
|
echo -e "${RED}✗ $name${RST}"
|
||||||
|
for i in "${issues[@]}"; do
|
||||||
|
echo -e " ${RED}→ $i${RST}"
|
||||||
|
done
|
||||||
|
local shown=0
|
||||||
|
for h in "${hints[@]}"; do
|
||||||
|
(( shown >= max_hints )) && { echo -e " ${YEL}~ … (${#hints[@]} avertissements au total)${RST}"; break; }
|
||||||
|
echo -e " ${YEL}~ $h${RST}"
|
||||||
|
(( shown++ ))
|
||||||
|
done
|
||||||
|
(( err++ ))
|
||||||
|
elif [[ ${#hints[@]} -gt 0 ]]; then
|
||||||
|
echo -e "${YEL}~ $name${RST}"
|
||||||
|
local shown=0
|
||||||
|
for h in "${hints[@]}"; do
|
||||||
|
(( shown >= max_hints )) && { echo -e " ${YEL}~ … (${#hints[@]} avertissements au total)${RST}"; break; }
|
||||||
|
echo -e " ${YEL}~ $h${RST}"
|
||||||
|
(( shown++ ))
|
||||||
|
done
|
||||||
|
(( warn++ ))
|
||||||
|
else
|
||||||
|
echo -e "${GRN}✓ $name${RST}"
|
||||||
|
(( ok++ ))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in "${DIRS[@]}"; do
|
||||||
|
[[ -d "$dir" ]] || continue
|
||||||
|
echo -e "\n${BLU}=== $dir ===${RST}"
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
check_file "$f"
|
||||||
|
done < <(find "$dir" -maxdepth 1 -name "*.desktop" -print0 | sort -z)
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "Résultat : ${GRN}$ok OK${RST} ${YEL}$warn avertissements${RST} ${RED}$err erreurs${RST}"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
tags:
|
||||||
|
- scripts
|
||||||
|
nom: verif-desktop
|
||||||
|
description: Audit des fichiers .desktop — chemins manquants, apps cachées, erreurs de syntaxe
|
||||||
|
---
|
||||||
|
|
||||||
|
# NOM
|
||||||
|
|
||||||
|
verif-desktop - Audit des fichiers .desktop de Linux Mint
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
verif-desktop
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
Vérifie l'ensemble des fichiers `.desktop` présents dans `~/.local/share/applications/` et `/usr/share/applications/`.
|
||||||
|
|
||||||
|
Pour chaque fichier, le script contrôle :
|
||||||
|
|
||||||
|
- **Exécutable manquant** : le chemin absolu dans `Exec=` n'existe pas sur le disque
|
||||||
|
- **Exécutable non exécutable** : le fichier existe mais n'a pas le bit `+x`
|
||||||
|
- **Commande introuvable** : la commande dans `Exec=` n'est pas dans le `$PATH`
|
||||||
|
- **App cachée** : `NoDisplay=true` ou `Hidden=true` dans le dossier utilisateur (l'app n'apparaît pas dans le menu)
|
||||||
|
- **Icône manquante** : `Icon=` pointe vers un chemin absolu inexistant
|
||||||
|
- **Erreurs de syntaxe** : signalées par `desktop-file-validate` (catégories invalides, clés dépréciées…)
|
||||||
|
|
||||||
|
Les gestionnaires d'URL (`MimeType=x-scheme-handler/...`) et les fichiers système sont traités différemment : `NoDisplay=true` y est attendu et n'est pas signalé comme erreur.
|
||||||
|
|
||||||
|
# COMPATIBILITÉ
|
||||||
|
|
||||||
|
Linux Mint (Cinnamon). Nécessite `desktop-file-utils` pour la validation syntaxique :
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install desktop-file-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
# EXEMPLES
|
||||||
|
|
||||||
|
Lancer l'audit complet :
|
||||||
|
```
|
||||||
|
verif-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
# CODES DE RETOUR
|
||||||
|
|
||||||
|
- `✓` vert — fichier valide
|
||||||
|
- `~` jaune — avertissement non bloquant
|
||||||
|
- `✗` rouge — erreur : l'application ne fonctionnera pas ou n'apparaîtra pas dans le menu
|
||||||
|
|
||||||
|
# VERSIONS
|
||||||
|
|
||||||
|
-26.05.1
|
||||||
|
: Version originale
|
||||||
|
|
||||||
|
# AUTEURS
|
||||||
|
|
||||||
|
Cédric Abonnel - \<canl.sb2023@acemail.fr>
|
||||||
|
|
||||||
|
# RAPPORT D'ERREURS
|
||||||
|
|
||||||
|
Pour signaler des erreurs ou des problèmes :
|
||||||
|
https://git.abonnel.fr/cedricAbonnel/scripts-bash
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
# - Modification du traitement des résumés des DOC. La fonctionnalité n'est pas encore accessible.
|
# - Modification du traitement des résumés des DOC. La fonctionnalité n'est pas encore accessible.
|
||||||
|
|
||||||
|
|
||||||
|
# Vérification du répertoire de travail
|
||||||
|
[[ ! -d "local/bin" ]] && { echo "Erreur : lancer depuis la racine du projet scripts-bash"; exit 1; }
|
||||||
|
|
||||||
# Spécifier le chemin du fichier journal
|
# Spécifier le chemin du fichier journal
|
||||||
log_dir="$HOME/log"
|
log_dir="$HOME/log"
|
||||||
log_file="${log_dir}/a5l-scripts_bash_prep-$(date '+%Y%m%d-%H%M%S')-$$.log"
|
log_file="${log_dir}/a5l-scripts_bash_prep-$(date '+%Y%m%d-%H%M%S')-$$.log"
|
||||||
@@ -42,9 +45,10 @@ create_dir() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create_dir "$log_dir"
|
||||||
|
|
||||||
log "Lister les fichiers présents dans local/bin"
|
log "Lister les fichiers présents dans local/bin"
|
||||||
ls local/bin/ -c1 >.config/files_local-bin
|
ls local/bin/ -1 >.config/files_local-bin
|
||||||
|
|
||||||
|
|
||||||
########
|
########
|
||||||
@@ -54,15 +58,15 @@ ls local/bin/ -c1 >.config/files_local-bin
|
|||||||
# 4. Lister des fichiers dans local/share/ytdll et share/ytdll/lib
|
# 4. Lister des fichiers dans local/share/ytdll et share/ytdll/lib
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire du dépôt Git local
|
# Spécifier le chemin du répertoire du dépôt Git local
|
||||||
source_dir="local/share/doc"
|
source_dir="local/share/doc/scripts-bash"
|
||||||
|
|
||||||
# Spécifier le chemin du répertoire
|
# Spécifier le chemin du répertoire
|
||||||
destination_dir="local/share/man"
|
destination_dir="local/share/man/man1"
|
||||||
|
|
||||||
create_dir "$destination_dir"
|
create_dir "$destination_dir"
|
||||||
|
|
||||||
log "Lister les fichiers présents dans local/share/doc"
|
log "Lister les fichiers présents dans local/share/doc/scripts-bash"
|
||||||
ls "$source_dir/" -c1 >.config/files_local-share-doc
|
ls "$source_dir/" -1 >.config/files_local-share-doc
|
||||||
|
|
||||||
log "Creer les pages MAN"
|
log "Creer les pages MAN"
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ if ! command -v pandoc &> /dev/null; then
|
|||||||
sudo dnf install -y pandoc
|
sudo dnf install -y pandoc
|
||||||
elif [ -x "$(command -v yum)" ]; then
|
elif [ -x "$(command -v yum)" ]; then
|
||||||
log "Installation de Pandoc via YUM (Red Hat)..."
|
log "Installation de Pandoc via YUM (Red Hat)..."
|
||||||
sudo dnf install -y pandoc
|
sudo yum install -y pandoc
|
||||||
elif [ -x "$(command -v apt)" ]; then
|
elif [ -x "$(command -v apt)" ]; then
|
||||||
log "Installation de Pandoc via APT (Debian)..."
|
log "Installation de Pandoc via APT (Debian)..."
|
||||||
sudo apt update && sudo apt install -y pandoc
|
sudo apt update && sudo apt install -y pandoc
|
||||||
@@ -100,10 +104,10 @@ log " Traitement des fichiers à copier dans $destination_dir"
|
|||||||
log " Vérifier si le fichier $file_local_desc existe"
|
log " Vérifier si le fichier $file_local_desc existe"
|
||||||
if [ -e "$file_local_desc" ]; then
|
if [ -e "$file_local_desc" ]; then
|
||||||
log " Récupérer des fichiers installés"
|
log " Récupérer des fichiers installés"
|
||||||
fileslist_local=$(cat $file_local_desc)
|
mapfile -t fileslist_local < "$file_local_desc"
|
||||||
|
|
||||||
log " Créer la page man"
|
log " Créer la page man"
|
||||||
for file in $fileslist_local; do
|
for file in "${fileslist_local[@]}"; do
|
||||||
new_name="${file%.*}" # Supprimer l'extension md
|
new_name="${file%.*}" # Supprimer l'extension md
|
||||||
if [ -f "$source_dir/$file" ]; then
|
if [ -f "$source_dir/$file" ]; then
|
||||||
pandoc -s "$source_dir/$file" -t man -o "$destination_dir/$new_name" || error "Impossible de créer la page 'man' $destination_dir/$new_name depuis $source_dir/$file"
|
pandoc -s "$source_dir/$file" -t man -o "$destination_dir/$new_name" || error "Impossible de créer la page 'man' $destination_dir/$new_name depuis $source_dir/$file"
|
||||||
@@ -114,37 +118,35 @@ else
|
|||||||
error " Le fichier $file_local_desc est introuvable."
|
error " Le fichier $file_local_desc est introuvable."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Lister les fichiers présents dans local/share/man"
|
log "Lister les fichiers présents dans local/share/man/man1"
|
||||||
ls local/share/man/ -c1 >.config/files_local-share-man
|
ls local/share/man/man1/ -1 >.config/files_local-share-man
|
||||||
|
|
||||||
log "Lister les fichiers présents dans local/share/ytdll"
|
log "Lister les fichiers présents dans local/share/ytdll"
|
||||||
ls local/share/ytdll/ -c1 >.config/files_local-share-ytdll
|
ls local/share/ytdll/ -1 >.config/files_local-share-ytdll
|
||||||
|
|
||||||
log "Lister les fichiers présents dans local/share/ytdll/lib"
|
log "Lister les fichiers présents dans local/share/ytdll/lib"
|
||||||
ls local/share/ytdll/lib/ -c1 >.config/files_local-share-ytdll-lib
|
ls local/share/ytdll/lib/ -1 >.config/files_local-share-ytdll-lib
|
||||||
|
|
||||||
########
|
########
|
||||||
# Résumé des DOC
|
# Résumé des DOC
|
||||||
|
|
||||||
|
|
||||||
for fichier in local/share/doc/*.md; do
|
for fichier in local/share/doc/scripts-bash/*.md; do
|
||||||
echo "Résumé pour $fichier :"
|
echo "Résumé pour $fichier :"
|
||||||
description_found=0
|
description_found=0
|
||||||
empty_line_encountered=false
|
|
||||||
description_started=0
|
description_started=0
|
||||||
|
|
||||||
while IFS= read -r ligne; do
|
while IFS= read -r ligne; do
|
||||||
if [ "$description_found" -eq 1 ] && [ "$description_started" -eq 1 ] && [ -n "$ligne" ]; then
|
if [ "$description_found" -eq 1 ]; then
|
||||||
echo "$ligne"
|
if [ -z "$ligne" ]; then
|
||||||
elif [ "$description_found" -eq 1 ] && [ "$description_started" -eq 0 ] && [ -n "$ligne" ]; then
|
[ "$description_started" -eq 1 ] && break
|
||||||
echo "$ligne"
|
else
|
||||||
description_started=1
|
description_started=1
|
||||||
elif [ "$description_found" -eq 1 ] && [ "$description_started" -eq 1 ] && [ ! -n "$ligne" ]; then
|
echo "$ligne"
|
||||||
break
|
fi
|
||||||
elif [ "$ligne" = "# DESCRIPTION" ]; then
|
elif [ "$ligne" = "# DESCRIPTION" ]; then
|
||||||
description_found=1
|
description_found=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done < "$fichier"
|
done < "$fichier"
|
||||||
|
|
||||||
echo "..."
|
echo "..."
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Audit de dérive des configurations PHP
|
* Nettoyeur de surcharge de configuration PHP
|
||||||
|
* Supprime les entrées locales identiques aux originales.
|
||||||
* Copyright (C) 2026 Cédric Abonnel
|
* Copyright (C) 2026 Cédric Abonnel
|
||||||
* License: GNU Affero General Public License v3
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- Initialisation & Sécurité ---
|
|
||||||
|
|
||||||
if (posix_getuid() !== 0) {
|
if (posix_getuid() !== 0) {
|
||||||
fwrite(STDERR, "Ce script doit être exécuté en root.\n");
|
fwrite(STDERR, "Ce script doit être exécuté en root.\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -16,89 +14,76 @@ if (posix_getuid() !== 0) {
|
|||||||
const CONF_DIR = '/opt/monitoring/conf';
|
const CONF_DIR = '/opt/monitoring/conf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrait les clés d'un fichier de configuration PHP (tableau return [])
|
* Logique d'affichage
|
||||||
* sans exécuter le fichier pour éviter les effets de bord, via Regex.
|
|
||||||
*/
|
*/
|
||||||
function extract_keys($file) {
|
function log_clean($level, $msg) {
|
||||||
$content = file_get_contents($file);
|
$color = ['info' => '34', 'notice' => '32', 'warn' => '33', 'err' => '31'][$level] ?? '37';
|
||||||
// On cherche les patterns : 'KEY' => ou "KEY" =>
|
echo sprintf("\e[%sm[%s]\e[0m %s\n", $color, strtoupper($level), $msg);
|
||||||
preg_match_all('/[\'"]([A-Z0-9_]+)[\'"]\s*=>/i', $content, $matches);
|
|
||||||
$keys = $matches[1] ?? [];
|
|
||||||
sort($keys);
|
|
||||||
return array_unique($keys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logique de logging
|
* Nettoie un fichier local en comparant ses valeurs avec la base
|
||||||
*/
|
*/
|
||||||
function log_audit($level, $event, $msg) {
|
function clean_local_config($base_file, $local_file) {
|
||||||
echo sprintf("[%s] %s: %s\n", strtoupper($level), $event, $msg);
|
// On inclut les fichiers pour récupérer les tableaux de config
|
||||||
}
|
$base_cfg = include $base_file;
|
||||||
|
$local_cfg = include $local_file;
|
||||||
|
|
||||||
function check_config_drift() {
|
if (!is_array($base_cfg) || !is_array($local_cfg)) {
|
||||||
$found_issue = false;
|
return 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
|
$new_local_cfg = $local_cfg;
|
||||||
$keys_base = extract_keys($base_conf);
|
$removed_keys = [];
|
||||||
$keys_local = extract_keys($local_conf);
|
|
||||||
|
|
||||||
$missing = array_diff($keys_base, $keys_local); // Présent dans base mais pas local
|
foreach ($local_cfg as $key => $value) {
|
||||||
$obsolete = array_diff($keys_local, $keys_base); // Présent dans local mais plus dans base
|
// Si la clé existe en base ET que la valeur est strictement identique
|
||||||
|
if (array_key_exists($key, $base_cfg) && $base_cfg[$key] === $value) {
|
||||||
if (!empty($missing) || !empty($obsolete)) {
|
unset($new_local_cfg[$key]);
|
||||||
$found_issue = true;
|
$removed_keys[] = $key;
|
||||||
$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 (empty($removed_keys)) {
|
||||||
if (!$found_issue) {
|
log_clean('info', basename($local_file) . " est déjà optimisé.");
|
||||||
log_audit('info', 'audit_success', "Toutes les configurations sont à jour ($reviewed_files fichiers vérifiés)");
|
return true;
|
||||||
} else {
|
|
||||||
log_audit('warning', 'audit_requires_action', "Action requise sur $files_requiring_action fichier(s) sur $reviewed_files");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconstruction du fichier PHP proprement
|
||||||
|
$content = "<?php\n/**\n * Configuration locale personnalisée\n * Généré par l'audit de nettoyage\n */\n\nreturn [\n";
|
||||||
|
foreach ($new_local_cfg as $key => $value) {
|
||||||
|
$val_export = var_export($value, true);
|
||||||
|
$content .= " '$key' => $val_export,\n";
|
||||||
|
}
|
||||||
|
$content .= "];\n";
|
||||||
|
|
||||||
|
if (file_put_contents($local_file, $content)) {
|
||||||
|
log_clean('notice', basename($local_file) . " nettoyé. Clés supprimées (identiques à la base) : " . implode(', ', $removed_keys));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main ---
|
// --- Main ---
|
||||||
check_config_drift();
|
|
||||||
|
log_clean('info', "Début de l'optimisation des configurations locales...");
|
||||||
|
|
||||||
|
$base_files = glob(CONF_DIR . '/*.php');
|
||||||
|
foreach ($base_files as $base_conf) {
|
||||||
|
// On ignore les fichiers .local.php et .conf.local.php
|
||||||
|
if (str_contains($base_conf, '.local.')) continue;
|
||||||
|
|
||||||
|
// Détermination du nom du fichier local correspondant
|
||||||
|
// Gère tes deux formats : monitoring.local.conf.php et alert-engine.conf.local.php
|
||||||
|
$local_conf = str_replace('.conf.php', '.conf.local.php', $base_conf);
|
||||||
|
if (!file_exists($local_conf)) {
|
||||||
|
$local_conf = str_replace('.php', '.local.php', $base_conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($local_conf)) {
|
||||||
|
clean_local_config($base_conf, $local_conf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_clean('info', "Optimisation terminée.");
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Mail
|
|
||||||
ALERT_MAIL_BIN="/usr/sbin/sendmail"
|
|
||||||
ALERT_MAIL_SUBJECT_PREFIX="[monitoring]"
|
|
||||||
|
|
||||||
# ntfy
|
|
||||||
NTFY_SERVER="https://ntfy.sh"
|
|
||||||
NTFY_TOPIC="TPOSOB84sBJ6HTZ7"
|
|
||||||
NTFY_TOKEN=""
|
|
||||||
|
|
||||||
# Déduplication en secondes
|
|
||||||
ALERT_DEDUP_WINDOW=3600
|
|
||||||
|
|
||||||
# Événements à ignorer
|
|
||||||
ALERT_IGNORE_EVENTS="update_not_needed alert_sent_ntfy alert_sent_mail"
|
|
||||||
|
|
||||||
# Règles par défaut selon le niveau
|
|
||||||
ALERT_DEFAULT_CHANNELS_WARNING="ntfy"
|
|
||||||
ALERT_DEFAULT_CHANNELS_ERROR="ntfy,mail"
|
|
||||||
ALERT_DEFAULT_CHANNELS_CRITICAL="ntfy,mail"
|
|
||||||
|
|
||||||
# Règles spécifiques par événement
|
|
||||||
ALERT_RULE_disk_usage_high="ntfy"
|
|
||||||
ALERT_RULE_disk_usage_critical="ntfy,mail"
|
|
||||||
ALERT_RULE_check_failed="ntfy,mail"
|
|
||||||
ALERT_RULE_internal_error="ntfy,mail"
|
|
||||||
|
|
||||||
ALERT_RULE_update_hash_unavailable="ntfy"
|
|
||||||
ALERT_RULE_update_download_failed="ntfy,mail"
|
|
||||||
ALERT_RULE_update_hash_mismatch="ntfy,mail"
|
|
||||||
ALERT_RULE_manifest_download_failed="ntfy,mail"
|
|
||||||
ALERT_RULE_manifest_invalid="ntfy,mail"
|
|
||||||
ALERT_RULE_update_finished_with_errors="ntfy,mail"
|
|
||||||
|
|
||||||
# Optionnel : certains événements peuvent être forcés en ntfy uniquement
|
|
||||||
# ALERT_RULE_disk_ok=""
|
|
||||||
# ALERT_RULE_update_finished=""
|
|
||||||
|
|
||||||
# Optionnel : URL ouverte quand on clique sur la notif ntfy
|
|
||||||
NTFY_CLICK_URL=""
|
|
||||||
|
|
||||||
# Optionnel : tags par niveau pour ntfy
|
|
||||||
NTFY_TAGS_WARNING="warning"
|
|
||||||
NTFY_TAGS_ERROR="warning,rotating_light"
|
|
||||||
NTFY_TAGS_CRITICAL="skull,warning"
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright (C) 2026 Cédric Abonnel
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
|
||||||
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
|
|
||||||
|
|
||||||
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
|
|
||||||
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
|
|
||||||
|
|
||||||
load_conf_if_exists "/opt/monitoring/conf/alert-engine.conf"
|
|
||||||
load_conf_if_exists "/opt/monitoring/conf/alert-engine.local.conf"
|
|
||||||
|
|
||||||
lock_or_exit "alert-engine"
|
|
||||||
require_cmd awk sed grep date tail stat cut tr
|
|
||||||
|
|
||||||
LOG_SOURCE="${LOG_FILE:-/var/log/monitoring/events.jsonl}"
|
|
||||||
STATE_FILE="${ALERT_STATE_FILE:-/var/lib/monitoring/alert-engine.offset}"
|
|
||||||
DEDUP_FILE="${ALERT_DEDUP_FILE:-/var/lib/monitoring/alert-engine.dedup}"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$DEDUP_FILE")" || fail_internal "Impossible de créer les répertoires d'état"
|
|
||||||
touch "$STATE_FILE" "$DEDUP_FILE" || fail_internal "Impossible d'initialiser les fichiers d'état"
|
|
||||||
|
|
||||||
json_get() {
|
|
||||||
local key="$1"
|
|
||||||
local line="$2"
|
|
||||||
|
|
||||||
printf '%s\n' "$line" \
|
|
||||||
| sed -n "s/.*\"${key}\":\"\([^\"]*\)\".*/\1/p" \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
json_get_number() {
|
|
||||||
local key="$1"
|
|
||||||
local line="$2"
|
|
||||||
|
|
||||||
printf '%s\n' "$line" \
|
|
||||||
| sed -n "s/.*\"${key}\":\([0-9][0-9]*\).*/\1/p" \
|
|
||||||
| head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
get_last_offset() {
|
|
||||||
local offset
|
|
||||||
offset="$(cat "$STATE_FILE" 2>/dev/null || true)"
|
|
||||||
if [[ "$offset" =~ ^[0-9]+$ ]]; then
|
|
||||||
printf '%s\n' "$offset"
|
|
||||||
else
|
|
||||||
printf '0\n'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
set_last_offset() {
|
|
||||||
printf '%s\n' "$1" > "$STATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
current_log_size() {
|
|
||||||
stat -c '%s' "$LOG_SOURCE" 2>/dev/null || printf '0\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_dedup_file() {
|
|
||||||
local now window tmp
|
|
||||||
now="$(date +%s)"
|
|
||||||
window="${ALERT_DEDUP_WINDOW:-3600}"
|
|
||||||
tmp="$(mktemp "${MONITORING_STATE_DIR}/alert-engine.dedup.XXXXXX")" || return 0
|
|
||||||
|
|
||||||
awk -F'|' -v now="$now" -v window="$window" '
|
|
||||||
NF >= 2 {
|
|
||||||
if ((now - $2) <= window) print $0
|
|
||||||
}
|
|
||||||
' "$DEDUP_FILE" > "$tmp" 2>/dev/null || true
|
|
||||||
|
|
||||||
mv -f "$tmp" "$DEDUP_FILE" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
dedup_key() {
|
|
||||||
local host="$1"
|
|
||||||
local app="$2"
|
|
||||||
local level="$3"
|
|
||||||
local event="$4"
|
|
||||||
printf '%s|%s|%s|%s\n' "$host" "$app" "$level" "$event"
|
|
||||||
}
|
|
||||||
|
|
||||||
should_notify_dedup() {
|
|
||||||
local key="$1"
|
|
||||||
local now window found_ts
|
|
||||||
now="$(date +%s)"
|
|
||||||
window="${ALERT_DEDUP_WINDOW:-3600}"
|
|
||||||
|
|
||||||
found_ts="$(awk -F'|' -v k="$key" '
|
|
||||||
$1 "|" $3 "|" $4 "|" $5 == k {print $2}
|
|
||||||
' "$DEDUP_FILE" | tail -n1)"
|
|
||||||
|
|
||||||
if [[ "$found_ts" =~ ^[0-9]+$ ]]; then
|
|
||||||
if [ $((now - found_ts)) -lt "$window" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
save_dedup_entry() {
|
|
||||||
local host="$1"
|
|
||||||
local app="$2"
|
|
||||||
local level="$3"
|
|
||||||
local event="$4"
|
|
||||||
local now
|
|
||||||
now="$(date +%s)"
|
|
||||||
printf '%s|%s|%s|%s|%s\n' "$host" "$now" "$app" "$level" "$event" >> "$DEDUP_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
event_is_ignored() {
|
|
||||||
local event="$1" ignored
|
|
||||||
for ignored in ${ALERT_IGNORE_EVENTS:-}; do
|
|
||||||
[ "$ignored" = "$event" ] && return 0
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
channels_for_event() {
|
|
||||||
local level="$1"
|
|
||||||
local event="$2"
|
|
||||||
local varname value
|
|
||||||
|
|
||||||
varname="ALERT_RULE_${event}"
|
|
||||||
value="${!varname:-}"
|
|
||||||
|
|
||||||
if [ -n "$value" ]; then
|
|
||||||
printf '%s\n' "$value"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$level" in
|
|
||||||
WARNING)
|
|
||||||
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_WARNING:-ntfy}"
|
|
||||||
;;
|
|
||||||
ERROR)
|
|
||||||
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_ERROR:-ntfy,mail}"
|
|
||||||
;;
|
|
||||||
CRITICAL)
|
|
||||||
printf '%s\n' "${ALERT_DEFAULT_CHANNELS_CRITICAL:-ntfy,mail}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf '\n'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
tags_for_level() {
|
|
||||||
case "$1" in
|
|
||||||
WARNING) printf '%s\n' "${NTFY_TAGS_WARNING:-warning}" ;;
|
|
||||||
ERROR) printf '%s\n' "${NTFY_TAGS_ERROR:-warning,rotating_light}" ;;
|
|
||||||
CRITICAL) printf '%s\n' "${NTFY_TAGS_CRITICAL:-skull,warning}" ;;
|
|
||||||
*) printf '\n' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
send_ntfy() {
|
|
||||||
|
|
||||||
local title="$1"
|
|
||||||
local body="$2"
|
|
||||||
local priority="$3"
|
|
||||||
|
|
||||||
[ "${ALERT_NTFY_ENABLED:-true}" = "true" ] || return 0
|
|
||||||
[ -n "${NTFY_SERVER:-}" ] || return 1
|
|
||||||
[ -n "${NTFY_TOPIC:-}" ] || return 1
|
|
||||||
|
|
||||||
local url="${NTFY_SERVER%/}/${NTFY_TOPIC}"
|
|
||||||
|
|
||||||
local curl_args=(
|
|
||||||
-fsS
|
|
||||||
-X POST
|
|
||||||
-H "Title: ${title}"
|
|
||||||
-H "Priority: ${priority}"
|
|
||||||
-H "Tags: warning"
|
|
||||||
-d "$body"
|
|
||||||
)
|
|
||||||
|
|
||||||
# topic protégé
|
|
||||||
if [ -n "${NTFY_TOKEN:-}" ]; then
|
|
||||||
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl "${curl_args[@]}" "$url" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
send_mail() {
|
|
||||||
local subject="$1"
|
|
||||||
local body="$2"
|
|
||||||
|
|
||||||
[ "${ALERT_MAIL_ENABLED:-true}" = "true" ] || return 0
|
|
||||||
[ -n "${DEST:-}" ] || return 1
|
|
||||||
[ -x "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" ] || return 1
|
|
||||||
|
|
||||||
{
|
|
||||||
printf 'To: %s\n' "${DEST}"
|
|
||||||
printf 'Subject: %s %s\n' "${ALERT_MAIL_SUBJECT_PREFIX:-[monitoring]}" "$subject"
|
|
||||||
printf 'Content-Type: text/plain; charset=UTF-8\n'
|
|
||||||
printf '\n'
|
|
||||||
printf '%s\n' "$body"
|
|
||||||
} | "${ALERT_MAIL_BIN:-/usr/sbin/sendmail}" -t
|
|
||||||
}
|
|
||||||
|
|
||||||
priority_for_level() {
|
|
||||||
case "$1" in
|
|
||||||
CRITICAL) printf 'urgent\n' ;;
|
|
||||||
ERROR) printf 'high\n' ;;
|
|
||||||
WARNING) printf 'default\n' ;;
|
|
||||||
*) printf 'default\n' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
build_title() {
|
|
||||||
local host="$1"
|
|
||||||
local app="$2"
|
|
||||||
local level="$3"
|
|
||||||
local event="$4"
|
|
||||||
printf '%s [%s] %s %s\n' "$host" "$app" "$level" "$event"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_body() {
|
|
||||||
local ts="$1"
|
|
||||||
local host="$2"
|
|
||||||
local app="$3"
|
|
||||||
local level="$4"
|
|
||||||
local event="$5"
|
|
||||||
local message="$6"
|
|
||||||
local line="$7"
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
Date: $ts
|
|
||||||
Hôte: $host
|
|
||||||
Script: $app
|
|
||||||
Niveau: $level
|
|
||||||
Événement: $event
|
|
||||||
|
|
||||||
Message:
|
|
||||||
$message
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
process_line() {
|
|
||||||
|
|
||||||
local line="$1"
|
|
||||||
local ts host app level event message channels title body prio ch key
|
|
||||||
|
|
||||||
ts="$(json_get "ts" "$line")"
|
|
||||||
host="$(json_get "host" "$line")"
|
|
||||||
app="$(json_get "app" "$line")"
|
|
||||||
level="$(json_get "level" "$line")"
|
|
||||||
event="$(json_get "event" "$line")"
|
|
||||||
message="$(json_get "message" "$line")"
|
|
||||||
|
|
||||||
local tags
|
|
||||||
tags="$(tags_for_level "$level")"
|
|
||||||
|
|
||||||
[ -n "$level" ] || return 0
|
|
||||||
[ -n "$event" ] || return 0
|
|
||||||
|
|
||||||
case "$level" in
|
|
||||||
DEBUG|INFO|NOTICE)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if event_is_ignored "$event"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
key="$(dedup_key "$host" "$app" "$level" "$event")"
|
|
||||||
if ! should_notify_dedup "$key"; then
|
|
||||||
log_debug "alert_suppressed_dedup" "Alerte supprimée par déduplication" \
|
|
||||||
"event=$event" "level=$level" "host=$host" "app=$app"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
channels="$(channels_for_event "$level" "$event")"
|
|
||||||
[ -n "$channels" ] || return 0
|
|
||||||
|
|
||||||
title="$(build_title "$host" "$app" "$level" "$event")"
|
|
||||||
body="$(build_body "$ts" "$host" "$app" "$level" "$event" "$message" "$line")"
|
|
||||||
prio="$(priority_for_level "$level")"
|
|
||||||
|
|
||||||
IFS=',' read -r -a channel_array <<< "$channels"
|
|
||||||
for ch in "${channel_array[@]}"; do
|
|
||||||
case "$ch" in
|
|
||||||
ntfy)
|
|
||||||
if send_ntfy "$title" "$body" "$prio" "$tags"; then
|
|
||||||
log_info "alert_sent_ntfy" "Notification ntfy envoyée" \
|
|
||||||
"event=$event" "level=$level" "host=$host" "app=$app"
|
|
||||||
else
|
|
||||||
log_error "alert_ntfy_failed" "Échec d'envoi ntfy" \
|
|
||||||
"event=$event" "level=$level" "host=$host" "app=$app"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
mail)
|
|
||||||
if send_mail "$title" "$body"; then
|
|
||||||
log_info "alert_sent_mail" "Mail d'alerte envoyé" \
|
|
||||||
"event=$event" "level=$level" "host=$host" "app=$app"
|
|
||||||
else
|
|
||||||
log_error "alert_mail_failed" "Échec d'envoi mail" \
|
|
||||||
"event=$event" "level=$level" "host=$host" "app=$app"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
save_dedup_entry "$host" "$app" "$level" "$event"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local last_offset log_size
|
|
||||||
last_offset="$(get_last_offset)"
|
|
||||||
log_size="$(current_log_size)"
|
|
||||||
|
|
||||||
if [ ! -f "$LOG_SOURCE" ]; then
|
|
||||||
log_notice "alert_log_missing" "Fichier de log absent, rien à traiter" "file=$LOG_SOURCE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$last_offset" -gt "$log_size" ]; then
|
|
||||||
log_notice "alert_offset_reset" "Offset réinitialisé après rotation ou troncature du log" \
|
|
||||||
"old_offset=$last_offset" "new_offset=0"
|
|
||||||
last_offset=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
cleanup_dedup_file
|
|
||||||
|
|
||||||
tail -c +$((last_offset + 1)) "$LOG_SOURCE" | while IFS= read -r line; do
|
|
||||||
[ -n "$line" ] || continue
|
|
||||||
process_line "$line"
|
|
||||||
done
|
|
||||||
|
|
||||||
set_last_offset "$log_size"
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
exit_with_status
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
MONITORING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
MONITORING_BASE_DIR="$(cd "${MONITORING_LIB_DIR}/.." && pwd)"
|
|
||||||
MONITORING_CONF_DIR="${MONITORING_BASE_DIR}/conf"
|
|
||||||
|
|
||||||
# Chargement config globale
|
|
||||||
if [ -f "${MONITORING_CONF_DIR}/monitoring.conf" ]; then
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
. "${MONITORING_CONF_DIR}/monitoring.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$0")}"
|
|
||||||
SCRIPT_PATH="${SCRIPT_PATH:-$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")}"
|
|
||||||
|
|
||||||
STATUS_OK=0
|
|
||||||
STATUS_WARNING=1
|
|
||||||
STATUS_ERROR=2
|
|
||||||
STATUS_INTERNAL=3
|
|
||||||
|
|
||||||
CURRENT_STATUS=$STATUS_OK
|
|
||||||
|
|
||||||
LOG_LEVEL=${LOG_LEVEL:-INFO}
|
|
||||||
|
|
||||||
json_escape() {
|
|
||||||
local s="${1:-}"
|
|
||||||
s="${s//\\/\\\\}"
|
|
||||||
s="${s//\"/\\\"}"
|
|
||||||
s="${s//$'\n'/\\n}"
|
|
||||||
s="${s//$'\r'/\\r}"
|
|
||||||
s="${s//$'\t'/\\t}"
|
|
||||||
printf '%s' "$s"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_event() {
|
|
||||||
local level="$1"
|
|
||||||
local event="$2"
|
|
||||||
local message="$3"
|
|
||||||
shift 3
|
|
||||||
|
|
||||||
local ts extra key value kv host
|
|
||||||
ts="$(date --iso-8601=seconds)"
|
|
||||||
|
|
||||||
# Détection dynamique du hostname si HOSTNAME_FQDN n'est pas défini
|
|
||||||
# On utilise 'hostname -f' pour le nom complet ou 'hostname' en secours
|
|
||||||
host="${HOSTNAME_FQDN:-$(hostname -f 2>/dev/null || hostname)}"
|
|
||||||
|
|
||||||
extra=""
|
|
||||||
|
|
||||||
for kv in "$@"; do
|
|
||||||
key="${kv%%=*}"
|
|
||||||
value="${kv#*=}"
|
|
||||||
extra="${extra},\"$(json_escape "$key")\":\"$(json_escape "$value")\""
|
|
||||||
done
|
|
||||||
|
|
||||||
# Utilisation de la variable 'host' détectée ci-dessus
|
|
||||||
printf '{"ts":"%s","host":"%s","app":"%s","level":"%s","event":"%s","message":"%s"%s}\n' \
|
|
||||||
"$(json_escape "$ts")" \
|
|
||||||
"$(json_escape "$host")" \
|
|
||||||
"$(json_escape "$SCRIPT_NAME")" \
|
|
||||||
"$(json_escape "$level")" \
|
|
||||||
"$(json_escape "$event")" \
|
|
||||||
"$(json_escape "$message")" \
|
|
||||||
"$extra" >> "${LOG_FILE:-/var/log/monitoring/events.jsonl}"
|
|
||||||
}
|
|
||||||
|
|
||||||
set_status() {
|
|
||||||
local new_status="$1"
|
|
||||||
if [ "$new_status" -gt "$CURRENT_STATUS" ]; then
|
|
||||||
CURRENT_STATUS="$new_status"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log_debug() { log_event "DEBUG" "$@"; }
|
|
||||||
log_info() { log_event "INFO" "$@"; }
|
|
||||||
log_notice() { log_event "NOTICE" "$@"; }
|
|
||||||
log_warning() { log_event "WARNING" "$@"; set_status "$STATUS_WARNING"; }
|
|
||||||
log_error() { log_event "ERROR" "$@"; set_status "$STATUS_ERROR"; }
|
|
||||||
log_critical() { log_event "CRITICAL" "$@"; set_status "$STATUS_ERROR"; }
|
|
||||||
|
|
||||||
fail_internal() {
|
|
||||||
log_event "ERROR" "internal_error" "$1"
|
|
||||||
exit "$STATUS_INTERNAL"
|
|
||||||
}
|
|
||||||
|
|
||||||
exit_with_status() {
|
|
||||||
exit "$CURRENT_STATUS"
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
local cmd
|
|
||||||
for cmd in "$@"; do
|
|
||||||
command -v "$cmd" >/dev/null 2>&1 || fail_internal "Commande requise absente: $cmd"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
load_conf_if_exists() {
|
|
||||||
local conf="$1"
|
|
||||||
[ -f "$conf" ] && . "$conf"
|
|
||||||
}
|
|
||||||
|
|
||||||
lock_or_exit() {
|
|
||||||
local lock_name="${1:-$SCRIPT_NAME}"
|
|
||||||
local lock_file="${MONITORING_LOCK_DIR:-/var/lock/monitoring}/${lock_name}.lock"
|
|
||||||
|
|
||||||
exec 9>"$lock_file" || fail_internal "Impossible d'ouvrir le lock $lock_file"
|
|
||||||
flock -n 9 || {
|
|
||||||
log_notice "already_running" "Une autre instance est déjà en cours" "lock=$lock_file"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
threshold_level() {
|
|
||||||
local value="$1"
|
|
||||||
local warning="$2"
|
|
||||||
local critical="$3"
|
|
||||||
|
|
||||||
if [ "$value" -ge "$critical" ]; then
|
|
||||||
printf 'CRITICAL'
|
|
||||||
elif [ "$value" -ge "$warning" ]; then
|
|
||||||
printf 'WARNING'
|
|
||||||
else
|
|
||||||
printf 'INFO'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
safe_mv() {
|
|
||||||
local src="$1"
|
|
||||||
local dst="$2"
|
|
||||||
mv -f "$src" "$dst" || fail_internal "Échec du déplacement de $src vers $dst"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_parent_dir() {
|
|
||||||
local file="$1"
|
|
||||||
mkdir -p "$(dirname "$file")" || fail_internal "Impossible de créer le répertoire parent de $file"
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright (C) 2026 Cédric Abonnel
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
|
||||||
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
|
|
||||||
|
|
||||||
# On s'assure d'avoir les permissions root
|
|
||||||
if [ "${EUID}" -ne 0 ]; then
|
|
||||||
echo "Ce script doit être exécuté en root." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
extract_keys() {
|
|
||||||
local file="$1"
|
|
||||||
grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$file" | cut -d'=' -f1 | sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
check_config_drift() {
|
|
||||||
local conf_dir="/opt/monitoring/conf"
|
|
||||||
local base_conf local_conf
|
|
||||||
local found_issue=false
|
|
||||||
local reviewed_files=0
|
|
||||||
local files_requiring_action=0
|
|
||||||
|
|
||||||
log_info "audit_start" "Début de l'audit des configurations locales"
|
|
||||||
|
|
||||||
while IFS= read -r base_conf; do
|
|
||||||
reviewed_files=$((reviewed_files + 1))
|
|
||||||
|
|
||||||
local_conf="${base_conf%.conf}.local.conf"
|
|
||||||
local file_name local_file_name
|
|
||||||
file_name="$(basename "$base_conf")"
|
|
||||||
local_file_name="$(basename "$local_conf")"
|
|
||||||
|
|
||||||
if [ ! -f "$local_conf" ]; then
|
|
||||||
cp "$base_conf" "$local_conf" || {
|
|
||||||
log_error "audit_create_local_failed" \
|
|
||||||
"Impossible de créer ${local_file_name} à partir de ${file_name}"
|
|
||||||
found_issue=true
|
|
||||||
files_requiring_action=$((files_requiring_action + 1))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
chmod 600 "$local_conf" 2>/dev/null || true
|
|
||||||
|
|
||||||
log_notice "audit_missing_local" \
|
|
||||||
"Le fichier ${local_file_name} n'existait pas ; il a été créé par copie de ${file_name}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local tmp_base tmp_local
|
|
||||||
tmp_base="$(mktemp)" || fail_internal "mktemp a échoué"
|
|
||||||
tmp_local="$(mktemp)" || fail_internal "mktemp a échoué"
|
|
||||||
|
|
||||||
extract_keys "$base_conf" > "$tmp_base"
|
|
||||||
extract_keys "$local_conf" > "$tmp_local"
|
|
||||||
|
|
||||||
local missing obsolete
|
|
||||||
missing="$(comm -23 "$tmp_base" "$tmp_local" | xargs)"
|
|
||||||
obsolete="$(comm -13 "$tmp_base" "$tmp_local" | xargs)"
|
|
||||||
|
|
||||||
if [ -n "$missing" ] || [ -n "$obsolete" ]; then
|
|
||||||
found_issue=true
|
|
||||||
files_requiring_action=$((files_requiring_action + 1))
|
|
||||||
|
|
||||||
log_warning "audit_file_requires_action" \
|
|
||||||
"Le fichier ${local_file_name} nécessite une vérification"
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
log_warning "audit_keys_missing" \
|
|
||||||
"Dans ${local_file_name}, options disponibles dans ${file_name} mais absentes du local : ${missing}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$obsolete" ]; then
|
|
||||||
log_info "audit_keys_obsolete" \
|
|
||||||
"Dans ${local_file_name}, options présentes uniquement dans le local et à vérifier ou supprimer : ${obsolete}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "audit_file_ok" \
|
|
||||||
"Le fichier ${local_file_name} contient les mêmes options que ${file_name}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmp_base" "$tmp_local"
|
|
||||||
done < <(find "$conf_dir" -maxdepth 1 -type f -name "*.conf" ! -name "*.local.conf" | sort)
|
|
||||||
|
|
||||||
if [ "$found_issue" = false ]; then
|
|
||||||
log_info "audit_success" \
|
|
||||||
"Toutes les configurations locales sont à jour (${reviewed_files} fichier(s) vérifié(s))"
|
|
||||||
else
|
|
||||||
log_warning "audit_requires_action" \
|
|
||||||
"Certaines configurations locales doivent être mises à jour (${files_requiring_action} fichier(s) à vérifier sur ${reviewed_files})"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
lock_or_exit "monitoring-audit"
|
|
||||||
check_config_drift
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
exit_with_status
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright (C) 2026 Cédric Abonnel
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# Moteur de mise à jour des programmes et fichiers connexes
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
|
||||||
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
|
|
||||||
|
|
||||||
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
|
|
||||||
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
|
|
||||||
|
|
||||||
load_conf_if_exists "/opt/monitoring/conf/autoupdate.conf"
|
|
||||||
load_conf_if_exists "/opt/monitoring/conf/autoupdate.local.conf"
|
|
||||||
|
|
||||||
# Définir les variables par défaut si elles ne sont pas dans les fichiers .conf
|
|
||||||
UPDATE_TMP_DIR="${UPDATE_TMP_DIR:-/tmp/monitoring-update}"
|
|
||||||
UPDATE_TIMEOUT_CONNECT="${UPDATE_TIMEOUT_CONNECT:-3}"
|
|
||||||
UPDATE_TIMEOUT_TOTAL="${UPDATE_TIMEOUT_TOTAL:-15}"
|
|
||||||
UPDATE_MANIFEST_URL="${UPDATE_MANIFEST_URL:-}"
|
|
||||||
UPDATE_BASE_URL="${UPDATE_BASE_URL:-}"
|
|
||||||
|
|
||||||
lock_or_exit "monitoring-update"
|
|
||||||
require_cmd curl sha256sum awk mktemp chmod dirname mv rm grep sed sort comm cut tr find
|
|
||||||
|
|
||||||
[ "${UPDATE_ENABLED:-true}" = "true" ] || {
|
|
||||||
log_notice "update_disabled" "Mise à jour désactivée par configuration"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "${UPDATE_TMP_DIR:-/tmp/monitoring-update}" || fail_internal "Impossible de créer le répertoire temporaire"
|
|
||||||
|
|
||||||
TMP_MANIFEST="$(mktemp "${UPDATE_TMP_DIR}/manifest.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
TMP_LOCAL_LIST="$(mktemp "${UPDATE_TMP_DIR}/local.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
TMP_REMOTE_LIST="$(mktemp "${UPDATE_TMP_DIR}/remote.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -f "$TMP_MANIFEST" "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
fetch_manifest() {
|
|
||||||
if ! curl -fsS \
|
|
||||||
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
|
|
||||||
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
|
|
||||||
"${UPDATE_MANIFEST_URL}" \
|
|
||||||
-o "$TMP_MANIFEST"; then
|
|
||||||
log_error "manifest_download_failed" \
|
|
||||||
"Impossible de télécharger le manifeste" \
|
|
||||||
"url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! awk '
|
|
||||||
NF == 3 &&
|
|
||||||
$1 ~ /^[0-9a-fA-F]{64}$/ &&
|
|
||||||
$2 ~ /^(644|755)$/ &&
|
|
||||||
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
|
|
||||||
$3 !~ /\.\./
|
|
||||||
' "$TMP_MANIFEST" >/dev/null; then
|
|
||||||
log_error "manifest_invalid" \
|
|
||||||
"Le manifeste distant est invalide" \
|
|
||||||
"url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "manifest_downloaded" "Manifeste téléchargé" "url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
list_remote_files() {
|
|
||||||
awk '{print $3}' "$TMP_MANIFEST" | sort -u > "$TMP_REMOTE_LIST"
|
|
||||||
}
|
|
||||||
|
|
||||||
list_local_files() {
|
|
||||||
find "${MONITORING_BASE_DIR}/bin" "${MONITORING_BASE_DIR}/lib" "${MONITORING_BASE_DIR}/conf" \
|
|
||||||
-type f 2>/dev/null \
|
|
||||||
| sed "s#^${MONITORING_BASE_DIR}/##" \
|
|
||||||
| sort -u > "$TMP_LOCAL_LIST"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply_mode() {
|
|
||||||
local mode="$1"
|
|
||||||
local file="$2"
|
|
||||||
|
|
||||||
case "$mode" in
|
|
||||||
755) chmod 755 "$file" ;;
|
|
||||||
644) chmod 644 "$file" ;;
|
|
||||||
*) fail_internal "Mode non supporté: $mode" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
update_one_file() {
|
|
||||||
local expected_hash="$1"
|
|
||||||
local mode="$2"
|
|
||||||
local rel_path="$3"
|
|
||||||
|
|
||||||
local local_file="${MONITORING_BASE_DIR}/${rel_path}"
|
|
||||||
local remote_file="${UPDATE_BASE_URL}/${rel_path}"
|
|
||||||
local tmp_file local_hash downloaded_hash
|
|
||||||
|
|
||||||
tmp_file="$(mktemp "${UPDATE_TMP_DIR}/$(basename "$rel_path").XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
|
|
||||||
if [ -f "$local_file" ]; then
|
|
||||||
local_hash="$(sha256sum "$local_file" | awk '{print $1}')"
|
|
||||||
else
|
|
||||||
local_hash=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$local_hash" = "$expected_hash" ]; then
|
|
||||||
log_debug "update_not_needed" "Fichier déjà à jour" "file=$rel_path"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! curl -fsS \
|
|
||||||
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
|
|
||||||
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
|
|
||||||
"$remote_file" \
|
|
||||||
-o "$tmp_file"; then
|
|
||||||
log_error "update_download_failed" \
|
|
||||||
"Téléchargement impossible" \
|
|
||||||
"file=$rel_path" "url=$remote_file"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
downloaded_hash="$(sha256sum "$tmp_file" | awk '{print $1}')"
|
|
||||||
if [ "$downloaded_hash" != "$expected_hash" ]; then
|
|
||||||
log_error "update_hash_mismatch" \
|
|
||||||
"Hash téléchargé invalide" \
|
|
||||||
"file=$rel_path" "expected=$expected_hash" "got=$downloaded_hash"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_parent_dir "$local_file"
|
|
||||||
apply_mode "$mode" "$tmp_file"
|
|
||||||
safe_mv "$tmp_file" "$local_file"
|
|
||||||
|
|
||||||
if [ -z "$local_hash" ]; then
|
|
||||||
log_notice "file_created" \
|
|
||||||
"Fichier créé depuis le manifeste" \
|
|
||||||
"file=$rel_path" "mode=$mode" "hash=$expected_hash"
|
|
||||||
else
|
|
||||||
log_notice "update_applied" \
|
|
||||||
"Mise à jour appliquée" \
|
|
||||||
"file=$rel_path" "mode=$mode" "old_hash=$local_hash" "new_hash=$expected_hash"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_extra_local_files() {
|
|
||||||
[ "${UPDATE_ALLOW_DELETE:-false}" = "true" ] || return 0
|
|
||||||
|
|
||||||
comm -23 "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST" | while IFS= read -r rel_path; do
|
|
||||||
[ -n "$rel_path" ] || continue
|
|
||||||
|
|
||||||
# Protection globale de TOUS les fichiers .local.conf
|
|
||||||
if [[ "$rel_path" == *.local.conf ]]; then
|
|
||||||
log_notice "delete_skipped" \
|
|
||||||
"Fichier local protégé (ignoré)" \
|
|
||||||
"file=$rel_path"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sécurité supplémentaire pour ne pas supprimer les répertoires vitaux
|
|
||||||
rm -f "${MONITORING_BASE_DIR}/${rel_path}" \
|
|
||||||
&& log_notice "file_deleted" \
|
|
||||||
"Fichier obsolète supprimé" \
|
|
||||||
"file=$rel_path" \
|
|
||||||
|| log_error "delete_failed" \
|
|
||||||
"Échec suppression" \
|
|
||||||
"file=$rel_path"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
run_local_conf_sync() {
|
|
||||||
|
|
||||||
local sync_script="${MONITORING_BASE_DIR}/bin/sync-local-confs.sh"
|
|
||||||
|
|
||||||
if [ -x "$sync_script" ]; then
|
|
||||||
log_info "local_conf_sync_start" \
|
|
||||||
"Synchronisation des fichiers .local.conf"
|
|
||||||
|
|
||||||
if "$sync_script"; then
|
|
||||||
log_info "local_conf_sync_done" \
|
|
||||||
"Synchronisation terminée"
|
|
||||||
else
|
|
||||||
log_warning "local_conf_sync_failed" \
|
|
||||||
"La synchronisation des .local.conf a échoué"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_notice "local_conf_sync_missing" \
|
|
||||||
"Script de synchronisation absent" \
|
|
||||||
"script=$sync_script"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local total=0 updated_or_checked=0 failed=0
|
|
||||||
local hash mode path
|
|
||||||
|
|
||||||
fetch_manifest || exit 2
|
|
||||||
list_remote_files
|
|
||||||
list_local_files
|
|
||||||
|
|
||||||
while read -r hash mode path; do
|
|
||||||
[ -n "${hash:-}" ] || continue
|
|
||||||
total=$((total + 1))
|
|
||||||
|
|
||||||
if update_one_file "$hash" "$mode" "$path"; then
|
|
||||||
updated_or_checked=$((updated_or_checked + 1))
|
|
||||||
else
|
|
||||||
failed=$((failed + 1))
|
|
||||||
fi
|
|
||||||
done < "$TMP_MANIFEST"
|
|
||||||
|
|
||||||
delete_extra_local_files
|
|
||||||
|
|
||||||
run_local_conf_sync
|
|
||||||
|
|
||||||
if [ "$failed" -gt 0 ]; then
|
|
||||||
log_warning "update_finished_with_errors" \
|
|
||||||
"Mise à jour terminée avec erreurs" \
|
|
||||||
"total=$total" "updated_or_checked=$updated_or_checked" "failed=$failed"
|
|
||||||
else
|
|
||||||
log_info "update_finished" \
|
|
||||||
"Mise à jour terminée" \
|
|
||||||
"total=$total" "updated_or_checked=$updated_or_checked" "failed=0"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
exit_with_status
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Configuration globale par défaut - NE PAS ÉDITER (Utilisez .local.conf)
|
|
||||||
|
|
||||||
MONITORING_BASE="/opt/monitoring"
|
|
||||||
MONITORING_LOG_DIR="/var/log/monitoring"
|
|
||||||
MONITORING_STATE_DIR="/var/lib/monitoring"
|
|
||||||
MONITORING_LOCK_DIR="/var/lock/monitoring"
|
|
||||||
|
|
||||||
LOG_FILE="${MONITORING_LOG_DIR}/events.jsonl"
|
|
||||||
|
|
||||||
HOSTNAME_FQDN="$(hostname -f 2>/dev/null || hostname)"
|
|
||||||
|
|
||||||
DEST="root"
|
|
||||||
|
|
||||||
NTFY_SERVER="nfy.sh"
|
|
||||||
NTFY_TOPIC="TPOSOB84sBJ6HTZ7"
|
|
||||||
NTFY_TOKEN=""
|
|
||||||
|
|
||||||
UPDATE_ENABLED="true"
|
|
||||||
UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring"
|
|
||||||
UPDATE_MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt"
|
|
||||||
UPDATE_TIMEOUT_CONNECT=3
|
|
||||||
UPDATE_TIMEOUT_TOTAL=15
|
|
||||||
UPDATE_TMP_DIR="/tmp/monitoring-update"
|
|
||||||
UPDATE_ALLOW_DELETE="false"
|
|
||||||
|
|
||||||
mkdir -p "$MONITORING_LOG_DIR" "$MONITORING_STATE_DIR" "$MONITORING_LOCK_DIR" 2>/dev/null || true
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
#
|
|
||||||
# lit le fichier log
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
SCRIPT_NAME="$(basename "$0")"
|
|
||||||
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
|
|
||||||
|
|
||||||
# shellcheck source=/opt/monitoring/lib/monitoring-lib.sh
|
|
||||||
. /opt/monitoring/lib/monitoring-lib.sh || exit 3
|
|
||||||
|
|
||||||
load_conf_if_exists "/opt/monitoring/conf/autoupdate.conf"
|
|
||||||
|
|
||||||
lock_or_exit "monitoring-update"
|
|
||||||
require_cmd curl sha256sum awk mktemp chmod dirname mv rm grep sed sort comm cut tr
|
|
||||||
|
|
||||||
[ "${UPDATE_ENABLED:-true}" = "true" ] || {
|
|
||||||
log_notice "update_disabled" "Mise à jour désactivée par configuration"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "${UPDATE_TMP_DIR:-/tmp/monitoring-update}" || fail_internal "Impossible de créer le répertoire temporaire"
|
|
||||||
|
|
||||||
TMP_MANIFEST="$(mktemp "${UPDATE_TMP_DIR}/manifest.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
TMP_LOCAL_LIST="$(mktemp "${UPDATE_TMP_DIR}/local.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
TMP_REMOTE_LIST="$(mktemp "${UPDATE_TMP_DIR}/remote.XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -f "$TMP_MANIFEST" "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
fetch_manifest() {
|
|
||||||
if ! curl -fsS \
|
|
||||||
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
|
|
||||||
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
|
|
||||||
"${UPDATE_MANIFEST_URL}" \
|
|
||||||
-o "$TMP_MANIFEST"; then
|
|
||||||
log_error "manifest_download_failed" "Impossible de télécharger le manifeste" "url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! awk '
|
|
||||||
NF == 3 &&
|
|
||||||
$1 ~ /^[0-9a-fA-F]{64}$/ &&
|
|
||||||
$2 ~ /^(644|755)$/ &&
|
|
||||||
$3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ &&
|
|
||||||
$3 !~ /\.\./
|
|
||||||
' "$TMP_MANIFEST" >/dev/null; then
|
|
||||||
log_error "manifest_invalid" "Le manifeste distant est invalide" "url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "manifest_downloaded" "Manifeste téléchargé" "url=${UPDATE_MANIFEST_URL}"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
list_remote_files() {
|
|
||||||
awk '{print $3}' "$TMP_MANIFEST" | sort -u > "$TMP_REMOTE_LIST"
|
|
||||||
}
|
|
||||||
|
|
||||||
list_local_files() {
|
|
||||||
find "${MONITORING_BASE_DIR}/bin" "${MONITORING_BASE_DIR}/lib" "${MONITORING_BASE_DIR}/conf" \
|
|
||||||
-type f 2>/dev/null \
|
|
||||||
| sed "s#^${MONITORING_BASE_DIR}/##" \
|
|
||||||
| sort -u > "$TMP_LOCAL_LIST"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply_mode() {
|
|
||||||
local mode="$1"
|
|
||||||
local file="$2"
|
|
||||||
|
|
||||||
case "$mode" in
|
|
||||||
755) chmod 755 "$file" ;;
|
|
||||||
644) chmod 644 "$file" ;;
|
|
||||||
*) fail_internal "Mode non supporté: $mode" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
update_one_file() {
|
|
||||||
local expected_hash="$1"
|
|
||||||
local mode="$2"
|
|
||||||
local rel_path="$3"
|
|
||||||
|
|
||||||
local local_file="${MONITORING_BASE_DIR}/${rel_path}"
|
|
||||||
local remote_file="${UPDATE_BASE_URL}/${rel_path}"
|
|
||||||
local tmp_file local_hash downloaded_hash
|
|
||||||
|
|
||||||
tmp_file="$(mktemp "${UPDATE_TMP_DIR}/$(basename "$rel_path").XXXXXX")" || fail_internal "mktemp a échoué"
|
|
||||||
|
|
||||||
if [ -f "$local_file" ]; then
|
|
||||||
local_hash="$(sha256sum "$local_file" | awk '{print $1}')"
|
|
||||||
else
|
|
||||||
local_hash=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$local_hash" = "$expected_hash" ]; then
|
|
||||||
log_debug "update_not_needed" "Fichier déjà à jour" "file=$rel_path"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! curl -fsS \
|
|
||||||
--connect-timeout "${UPDATE_TIMEOUT_CONNECT:-3}" \
|
|
||||||
--max-time "${UPDATE_TIMEOUT_TOTAL:-15}" \
|
|
||||||
"$remote_file" \
|
|
||||||
-o "$tmp_file"; then
|
|
||||||
log_error "update_download_failed" "Téléchargement impossible" "file=$rel_path" "url=$remote_file"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
downloaded_hash="$(sha256sum "$tmp_file" | awk '{print $1}')"
|
|
||||||
if [ "$downloaded_hash" != "$expected_hash" ]; then
|
|
||||||
log_error "update_hash_mismatch" "Hash téléchargé invalide" \
|
|
||||||
"file=$rel_path" "expected=$expected_hash" "got=$downloaded_hash"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_parent_dir "$local_file"
|
|
||||||
apply_mode "$mode" "$tmp_file"
|
|
||||||
safe_mv "$tmp_file" "$local_file"
|
|
||||||
|
|
||||||
if [ -z "$local_hash" ]; then
|
|
||||||
log_notice "file_created" "Fichier créé depuis le manifeste" \
|
|
||||||
"file=$rel_path" "mode=$mode" "hash=$expected_hash"
|
|
||||||
else
|
|
||||||
log_notice "update_applied" "Mise à jour appliquée" \
|
|
||||||
"file=$rel_path" "mode=$mode" "old_hash=$local_hash" "new_hash=$expected_hash"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_extra_local_files() {
|
|
||||||
|
|
||||||
[ "${UPDATE_ALLOW_DELETE:-false}" = "true" ] || return 0
|
|
||||||
|
|
||||||
comm -23 "$TMP_LOCAL_LIST" "$TMP_REMOTE_LIST" | while IFS= read -r rel_path; do
|
|
||||||
|
|
||||||
[ -n "$rel_path" ] || continue
|
|
||||||
|
|
||||||
case "$rel_path" in
|
|
||||||
conf/autoupdate.conf|conf/alert-engine.local.conf)
|
|
||||||
log_notice "delete_skipped" \
|
|
||||||
"Suppression ignorée pour fichier local protégé" \
|
|
||||||
"file=$rel_path"
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
rm -f "${MONITORING_BASE_DIR}/${rel_path}" \
|
|
||||||
&& log_notice "file_deleted" \
|
|
||||||
"Fichier supprimé car absent du manifeste" \
|
|
||||||
"file=$rel_path" \
|
|
||||||
|| log_error "delete_failed" \
|
|
||||||
"Impossible de supprimer fichier local" \
|
|
||||||
"file=$rel_path"
|
|
||||||
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local total=0 updated=0 failed=0 hash mode path
|
|
||||||
|
|
||||||
fetch_manifest || exit 2
|
|
||||||
list_remote_files
|
|
||||||
list_local_files
|
|
||||||
|
|
||||||
while read -r hash mode path; do
|
|
||||||
[ -n "${hash:-}" ] || continue
|
|
||||||
total=$((total + 1))
|
|
||||||
|
|
||||||
if update_one_file "$hash" "$mode" "$path"; then
|
|
||||||
updated=$((updated + 1))
|
|
||||||
else
|
|
||||||
failed=$((failed + 1))
|
|
||||||
fi
|
|
||||||
done < "$TMP_MANIFEST"
|
|
||||||
|
|
||||||
delete_extra_local_files
|
|
||||||
|
|
||||||
if [ "$failed" -gt 0 ]; then
|
|
||||||
log_warning "update_finished_with_errors" \
|
|
||||||
"Mise à jour terminée avec erreurs" \
|
|
||||||
"total=$total" "updated_or_checked=$updated" "failed=$failed"
|
|
||||||
else
|
|
||||||
log_info "update_finished" \
|
|
||||||
"Mise à jour terminée" \
|
|
||||||
"total=$total" "updated_or_checked=$updated" "failed=0"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
exit_with_status
|
|
||||||
Reference in New Issue
Block a user