=== Script de synchronisation PiHole ====
== Vue d'ensemble ==
Ce script permet de synchroniser une configuration PiHole depuis un serveur Mikrotik (source de vérité) vers deux serveurs Ubuntu. Il gère la copie des fichiers de configuration, des bases de données et des règles DNS personnalisées.
== Architecture ==
=== Serveurs ===
* '''Source de vérité''' : Mikrotik (192.168.2.1)
** Stockage : /usb1/etc/pihole/ et /usb1/etc/dnsmasq.d/
** Utilisateur : piholesync
* '''Serveurs cibles''' : Ubuntu (192.168.2.8 et 192.168.2.9)
** Stockage : /etc/pihole/ et /etc/dnsmasq.d/
** Utilisateur : piholesync avec droits sudo
=== Fichiers synchronisés ===
* '''PiHole''' :
** gravity.db (base de données principale)
** custom.list (règles personnalisées)
** local.list (règles locales)
* '''DNSMasq''' :
** 05-pihole-custom-cname.conf (règles CNAME)
== Fonctionnalités ==
=== Système de backup ===
* Localisation : /var/backup/pihole-sync/
* Rotation automatique : Conservation de 7 jours
* Structure : Un sous-dossier par serveur et par date
* Contenu : Copie complète de /etc/pihole et /etc/dnsmasq.d
=== Logging ===
* Fichiers : /var/log/pihole-sync/sync.log
* Rotation : Quotidienne avec conservation de 7 jours
* Format : [TIMESTAMP] Message
* Niveaux : Info et Debug (avec --debug)
=== Sécurité ===
* Authentification SSH par clés
* Utilisateur dédié (piholesync)
* Permissions sudo limitées
* Vérification des droits avant exécution
== Processus de synchronisation ==
=== 1. Vérifications préalables ===
# Rotation des logs
# Vérification de l'existence des fichiers source
# Test des droits sudo sur tous les serveurs
# Création des backups
=== 2. Copie depuis Mikrotik ===
# Création d'un dossier temporaire
# Copie des fichiers via SFTP
# Vérification de l'intégrité (taille, format SQLite)
# Synchronisation du fichier CNAME
=== 3. Déploiement ===
# Arrêt des services PiHole
# Synchronisation des fichiers via rsync
# Correction des permissions
# Reconstruction des bases gravity
# Redémarrage des services
== Utilisation ==
=== Prérequis ===
# Configuration SSH avec clés pour piholesync
# Droits sudo pour piholesync sur les commandes :
#* rsync
#* pihole
# Répertoires de logs et backups créés avec bonnes permissions
=== Commandes ===
# Exécution normale
./pihole-sync.sh
# Mode simulation
./pihole-sync.sh --dry-run
# Mode debug
./pihole-sync.sh --debug
# Aide
./pihole-sync.sh --help
=== Options ===
* '''--dry-run''' : Simulation sans modification
* '''--debug''' : Affichage des messages de debug
== Résolution des problèmes ==
=== Logs à vérifier ===
# /var/log/pihole-sync/sync.log
# Journaux système (journalctl)
# Logs PiHole (/var/log/pihole/pihole.log)
=== Problèmes courants ===
; Erreur SSH
: Vérifier les clés et permissions
; Erreur sudo
: Contrôler le fichier sudoers
; Fichiers invalides
: Vérifier l'espace disque et les permissions
; Services non redémarrés
: Relancer manuellement pihole -g
== Maintenance ==
=== Tâches quotidiennes ===
* Vérification des logs
* Contrôle des backups
* Validation du fonctionnement DNS
=== Tâches hebdomadaires ===
* Nettoyage des vieux backups
* Vérification de l'espace disque
* Test de restauration
=== Tâches mensuelles ===
* Rotation des clés SSH
* Audit des permissions
* Test de recovery complet
#!/bin/bash
# Variables de configuration
MIKROTIK_MASTER="piholesync@192.168.2.1"
LOCAL_HOST="192.168.2.8"
OTHER_UBUNTU="piholesync@192.168.2.9"
DRY_RUN=0
DEBUG=0
# SSH Options
SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=no"
SUDO_OPTS=" sudo -n -u piholesync "
# Chemins sur le Mikrotik (master)
MIKROTIK_PIHOLE_PATH="/usb1/etc"
MIKROTIK_DNSMASQ_PATH="/usb1/etc-dnsmasq.d"
# Chemins de logs et backups
LOG_DIR="/var/log/pihole-sync"
LOG_FILE="$LOG_DIR/sync.log"
BACKUP_DIR="/var/backup/pihole-sync"
MAX_LOGS=7
# Liste des fichiers à exclure
EXCLUDE_LIST=(
"pihole-FTL-old.db"
"gravity_old.db"
"*.log"
"dhcp.leases"
"setupVars.conf.update.bak"
"pihole-FTL.conf"
"install.log"
)
# Fonction pour construire les options rsync
build_rsync_opts() {
local target="$1" # Paramètre pour identifier la cible
local opts="-avz"
# Ajouter les exclusions
for item in "${EXCLUDE_LIST[@]}"; do
opts+=" --exclude=$item"
done
# Ajouter --dry-run si nécessaire
[ $DRY_RUN -eq 1 ] && opts+=" --dry-run"
# Option spéciale pour Mikrotik
if [[ $target == *"192.168.2.1"* ]]; then
opts+=" --rsh='ssh $SSH_OPTS' --rsync-path=/usr/bin/rsync"
fi
echo "$opts"
}
# Fonction de debug
debug() {
if [ $DEBUG -eq 1 ]; then
echo "DEBUG: $1"
fi
}
# Fonction de logging
log() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message"
if [ -w "$LOG_DIR" ]; then
echo "[$timestamp] $message" >> "$LOG_FILE"
fi
}
# Fonction de rotation des logs
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d')"
find "$LOG_DIR" -name "sync.log.*" -mtime +$MAX_LOGS -delete
fi
}
# Fonction de vérification des droits sudo
check_sudo() {
local host="$1"
log "Vérification des droits sudo sur $host..."
if [[ $host == "localhost" ]]; then
# Test local
log "Test des permissions rsync..."
if $SUDO_OPTS rsync --version >/dev/null 2>&1; then
log "✓ Droits sudo OK pour rsync"
else
log "✗ Droits sudo manquants pour rsync"
return 1
fi
log "Test des permissions pihole..."
if $SUDO_OPTS pihole -v >/dev/null 2>&1; then
log "✓ Droits sudo OK pour pihole"
else
log "✗ Droits sudo manquants pour pihole"
return 1
fi
else
# Test distant avec sudo -u piholesync
log "Test des permissions rsync sur $host..."
if sudo -u piholesync ssh $SSH_OPTS "$host" "$SUDO_OPTS rsync --version" >/dev/null 2>&1; then
log "✓ Droits sudo OK pour rsync sur $host"
else
log "✗ Droits sudo manquants pour rsync sur $host"
return 1
fi
log "Test des permissions pihole sur $host..."
if sudo -u piholesync ssh $SSH_OPTS "$host" "$SUDO_OPTS pihole -v" >/dev/null 2>&1; then
log "✓ Droits sudo OK pour pihole sur $host"
else
log "✗ Droits sudo manquants pour pihole sur $host"
return 1
fi
fi
log "✓ Tous les droits sudo sont corrects"
return 0
}
# Fonction de backup modifiée
backup_pihole() {
local host="$1"
local backup_date=$(date '+%Y%m%d_%H%M%S')
local backup_path="$BACKUP_DIR/$host/$backup_date"
local rsync_opts=$(build_rsync_opts "$host")
log "Création d'une sauvegarde pour $host"
# Création des répertoires avec les bonnes permissions
if ! sudo mkdir -p "$backup_path/pihole" "$backup_path/dnsmasq.d"; then
log "Erreur : Impossible de créer les répertoires de backup"
return 1
fi
# Attribution des permissions à piholesync
if ! sudo chown -R piholesync:piholesync "$backup_path"; then
log "Erreur : Impossible de modifier les permissions des répertoires de backup"
return 1
fi
if [[ $host == "localhost" ]]; then
if ! sudo -u piholesync rsync $rsync_opts /etc/pihole/ "$backup_path/pihole/"; then
log "Erreur : Échec de la sauvegarde des fichiers pihole locaux"
return 1
fi
if ! sudo -u piholesync rsync $rsync_opts /etc/dnsmasq.d/ "$backup_path/dnsmasq.d/"; then
log "Erreur : Échec de la sauvegarde des fichiers dnsmasq locaux"
return 1
fi
else
if ! sudo -u piholesync rsync $rsync_opts -e "ssh $SSH_OPTS" "$host:/etc/pihole/" "$backup_path/pihole/"; then
log "Erreur : Échec de la sauvegarde des fichiers pihole distants"
return 1
fi
if ! sudo -u piholesync rsync $rsync_opts -e "ssh $SSH_OPTS" "$host:/etc/dnsmasq.d/" "$backup_path/dnsmasq.d/"; then
log "Erreur : Échec de la sauvegarde des fichiers dnsmasq distants"
return 1
fi
fi
log "Sauvegarde terminée pour $host dans $backup_path"
return 0
}
# Fonction pour vérifier l'existence d'un fichier distant via SSH
# Fonction pour vérifier l'existence d'un fichier distant via SSH
check_remote_file() {
local host="$1"
local file="$2"
local description="$3"
log "Vérification de $description sur $host..."
if [[ $host == *"192.168.2.1"* ]]; then
# Cas spécial pour Mikrotik
local mikrotik_path=${file#/}
local cmd="/file print where name=\"$mikrotik_path\""
debug "Chemin original: $file"
debug "Chemin Mikrotik: $mikrotik_path"
debug "Commande exécutée: $cmd"
local output=$(sudo -u piholesync ssh $SSH_OPTS "$host" "$cmd")
debug "Sortie de la commande:"
debug "$output"
if echo "$output" | grep -q "$mikrotik_path"; then
log "✓ $description trouvé"
return 0
else
log "✗ $description non trouvé ($mikrotik_path)"
return 1
fi
else
# Cas standard pour les serveurs Ubuntu
if sudo -u piholesync ssh $SSH_OPTS "$host" "$SUDO_OPTS [ -f $file ]"; then
log "✓ $description trouvé"
return 0
else
log "✗ $description non trouvé à $file"
return 1
fi
fi
}
# Fonction pour copier depuis le Mikrotik
# Fonction pour copier depuis le Mikrotik
copy_from_mikrotik() {
local source="$1"
local dest="$2"
local description="$3"
log "Copie de $description depuis Mikrotik..."
if [ $DRY_RUN -eq 1 ]; then
log "[DRY RUN] Copie de $source vers $dest"
return 0
fi
# Création d'un répertoire temporaire
local temp_dir=$(mktemp -d)
chown piholesync "$temp_dir" # Ajout de cette ligne !
local temp_file="$temp_dir/$(basename "$dest")"
debug "Tentative de copie avec SFTP..."
# Exécution de la commande SFTP
if ! sudo -u piholesync sftp "$MIKROTIK_MASTER" <<< "get $source $temp_file"; then
log "✗ Erreur lors de la copie de $description via SFTP"
rm -rf "$temp_dir"
return 1
fi
# Vérification de la taille du fichier
local file_size=$(stat -c%s "$temp_file" 2>/dev/null || stat -f%z "$temp_file")
debug "Taille du fichier temporaire: $file_size octets"
# Vérification SQLite pour gravity.db
if [[ "$source" == *"gravity.db"* ]]; then
if ! sqlite3 "$temp_file" "SELECT name FROM sqlite_master LIMIT 1" > /dev/null 2>&1; then
log "✗ Le fichier temporaire $description n'est pas une base SQLite valide"
rm -rf "$temp_dir"
return 1
fi
debug "Vérification SQLite réussie"
fi
# Déplacement du fichier vers sa destination finale
if ! mv "$temp_file" "$dest"; then
log "✗ Erreur lors du déplacement de $description vers sa destination finale"
rm -rf "$temp_dir"
return 1
fi
# Nettoyage
rm -rf "$temp_dir"
# Vérification finale
if [ ! -s "$dest" ]; then
log "✗ Le fichier $description est vide après copie"
return 1
fi
log "✓ Copie de $description réussie (taille: $file_size octets)"
return 0
}
# Fonction pour vérifier la taille d'un fichier sur le Mikrotik
check_mikrotik_file_size() {
local file="$1"
local min_size="$2"
log "Vérification de la taille de $file..."
# Obtenir les informations du fichier via ssh
local file_info=$(sudo -u piholesync ssh $SSH_OPTS "$MIKROTIK_MASTER" "du -b \"$file\" 2>/dev/null || echo '0'")
# Extraire la taille (premier champ de la sortie du)
local size=$(echo "$file_info" | awk '{print $1}')
if [ -z "$size" ] || [ "$size" -lt "$min_size" ]; then
log "✗ Le fichier $file sur le Mikrotik est trop petit ou inexistant (${size:-0} octets < ${min_size} octets attendus)"
return 1
fi
log "✓ Taille du fichier $file sur le Mikrotik: ${size} octets"
return 0
}
# Fonction pour arrêter/démarrer/reconstruire Pi-hole
manage_pihole_service() {
local host="$1"
local action="$2"
local description="$3"
log "$description sur $host..."
if [[ $host == "localhost" ]]; then
case $action in
"stop")
sudo pihole -f stop
;;
"start")
sudo pihole -f start
;;
"rebuild")
sudo pihole -g
;;
esac
else
case $action in
"stop")
sudo -u piholesync ssh -t "$host" "$SUDO_OPTS pihole -f stop"
;;
"start")
sudo -u piholesync ssh -t "$host" "$SUDO_OPTS pihole -f start"
;;
"rebuild")
sudo -u piholesync ssh -t "$host" "$SUDO_OPTS pihole -g"
;;
esac
fi
}
sync_to_ubuntu() {
local src="$1"
local dest="$2"
local host="$3"
local description="$4"
log "Synchronisation de $description vers $host..."
if [ $DRY_RUN -eq 1 ]; then
log "[DRY RUN] Synchronisation vers $host"
return 0
fi
if [[ $host == "localhost" ]]; then
# Vérification et création du répertoire
if [ ! -d "$dest" ]; then
sudo mkdir -p "$dest"
fi
# Exécution de rsync en sudo pour éviter les problèmes de permissions
sudo rsync -avz --no-perms --no-owner --no-times "$src" "$dest" || true
# Correction des permissions pour gravity.db
if [[ -f "$dest/gravity.db" && "$EUID" -eq 0 ]]; then
sudo chown pihole:pihole "$dest/gravity.db"
sudo chmod 664 "$dest/gravity.db"
fi
else
# Synchronisation distante avec rsync
sudo -u piholesync rsync -avz --no-perms --no-owner --no-times "$src" "$host:$dest/" || true
# Correction des permissions sur le serveur distant
if [[ "$dest" == *"pihole"* ]]; then
if [[ -n "$SUDO_OPTS" ]]; then
sudo -u piholesync ssh $SSH_OPTS "$host" "$SUDO_OPTS chown pihole:pihole $dest/gravity.db"
sudo -u piholesync ssh $SSH_OPTS "$host" "$SUDO_OPTS chmod 664 $dest/gravity.db"
else
log "⚠️ Impossible de changer les permissions sur $host, SUDO_OPTS est vide."
fi
fi
fi
}
sync_dnsmasq_cname() {
local cname_file="05-pihole-custom-cname.conf"
local src_path="usb1/etc/dnsmasq.d/$cname_file" # Correction du chemin ici
log "Vérification du fichier CNAME ($cname_file)..."
# Vérifie si le fichier existe sur le Mikrotik en utilisant /file get
local check_output
check_output=$(sudo -u piholesync ssh $SSH_OPTS "$MIKROTIK_MASTER" ":put [/file/get \"$src_path\" contents]" 2>/dev/null)
debug "Résultat de la vérification: '$check_output'"
if [ -n "$check_output" ] && [[ ! "$check_output" =~ "no such item" ]]; then
log "✓ Fichier CNAME trouvé sur le Mikrotik, synchronisation..."
if [ $DRY_RUN -eq 0 ]; then
# Crée le répertoire temporaire s'il n'existe pas
mkdir -p "$TEMP_DIR/dnsmasq.d"
# Écrit le contenu directement dans le fichier temporaire
echo "$check_output" > "$TEMP_DIR/dnsmasq.d/$cname_file"
# Synchronise vers les deux serveurs Ubuntu
sync_to_ubuntu "$TEMP_DIR/dnsmasq.d/$cname_file" "/etc/dnsmasq.d/" "localhost" "fichier CNAME local"
sync_to_ubuntu "$TEMP_DIR/dnsmasq.d/$cname_file" "/etc/dnsmasq.d/" "$OTHER_UBUNTU" "fichier CNAME distant"
else
log "[DRY RUN] Le fichier CNAME serait synchronisé"
fi
else
log "! Fichier CNAME non trouvé sur le Mikrotik, suppression sur les serveurs Ubuntu..."
if [ $DRY_RUN -eq 0 ]; then
# Supprime le fichier sur les deux serveurs Ubuntu s'il existe
sudo rm -f "/etc/dnsmasq.d/$cname_file"
sudo -u piholesync ssh $SSH_OPTS "$OTHER_UBUNTU" "$SUDO_OPTS rm -f /etc/dnsmasq.d/$cname_file"
else
log "[DRY RUN] Le fichier CNAME serait supprimé des serveurs Ubuntu"
fi
fi
}
# Fonction de rotation des logs
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d')"
find "$LOG_DIR" -name "sync.log.*" -mtime +$MAX_LOGS -delete
fi
}
# Fonction pour effectuer les backups
perform_backups() {
if [ $DRY_RUN -eq 0 ]; then
log "Démarrage des sauvegardes..."
backup_pihole "localhost"
backup_pihole "$OTHER_UBUNTU"
else
log "[DRY RUN] Les sauvegardes seraient effectuées ici"
fi
}
# Traitement des arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=1
shift
;;
--debug)
DEBUG=1
shift
;;
--help)
echo "Usage: $0 [--dry-run] [--debug]"
echo "Options:"
echo " --dry-run Simuler la synchronisation sans l'exécuter"
echo " --debug Afficher les messages de debug"
exit 0
;;
*)
echo "Option invalide: $1"
echo "Utilisez --help pour voir les options disponibles"
exit 1
;;
esac
done
log "Mode: $([ $DRY_RUN -eq 1 ] && echo 'DRY RUN' || echo 'PRODUCTION')$([ $DEBUG -eq 1 ] && echo ' (DEBUG activé)')"
# Rotation des logs au démarrage
rotate_logs
# Vérification des prérequis
log "Vérification des prérequis..."
check_remote_file "$MIKROTIK_MASTER" "$MIKROTIK_PIHOLE_PATH/pihole/gravity.db" "gravity.db" || exit 1
check_remote_file "$MIKROTIK_MASTER" "$MIKROTIK_PIHOLE_PATH/pihole/custom.list" "custom.list" || exit 1
check_remote_file "$MIKROTIK_MASTER" "$MIKROTIK_PIHOLE_PATH/pihole/local.list" "local.list" || exit 1
# Vérification des droits sudo
log "Vérification des droits sudo..."
check_sudo "localhost" || exit 1
check_sudo "$OTHER_UBUNTU" || exit 1
# Effectuer les backups avant synchronisation
perform_backups
# Création du dossier temporaire
TEMP_DIR="/tmp/pihole-sync"
mkdir -p "$TEMP_DIR/pihole"
mkdir -p "$TEMP_DIR/dnsmasq.d"
# Synchronisation depuis le Mikrotik
log "Synchronisation depuis $MIKROTIK_MASTER..."
# Copie des fichiers depuis le Mikrotik
mkdir -p "$TEMP_DIR/pihole" "$TEMP_DIR/dnsmasq.d"
copy_from_mikrotik "usb1/etc/pihole/gravity.db" "$TEMP_DIR/pihole/gravity.db" "gravity.db" || exit 1
copy_from_mikrotik "usb1/etc/pihole/custom.list" "$TEMP_DIR/pihole/custom.list" "custom.list" || exit 1
copy_from_mikrotik "usb1/etc/pihole/local.list" "$TEMP_DIR/pihole/local.list" "local.list" || exit 1
# Pour le dossier dnsmasq.d
log "Gestion du fichier CNAME..."
if [ $DRY_RUN -eq 1 ]; then
log "[DRY RUN] Vérification et synchronisation du fichier CNAME"
else
sync_dnsmasq_cname || exit 1
fi
# Dans la partie synchronisation :
if [ $DRY_RUN -eq 0 ]; then
# Arrêt des services sur les deux serveurs
manage_pihole_service "localhost" "stop" "Arrêt du service Pi-hole"
manage_pihole_service "$OTHER_UBUNTU" "stop" "Arrêt du service Pi-hole distant"
log "Synchronisation vers les serveurs Ubuntu..."
# Synchronisation locale
sync_to_ubuntu "$TEMP_DIR/pihole/" "/etc/pihole/" "localhost" "fichiers pihole locaux" || exit 1
sync_to_ubuntu "$TEMP_DIR/dnsmasq.d/" "/etc/dnsmasq.d/" "localhost" "dossier dnsmasq.d local" || exit 1
# Synchronisation vers le serveur distant
sync_to_ubuntu "$TEMP_DIR/pihole/" "/etc/pihole" "$OTHER_UBUNTU" "fichiers pihole vers $OTHER_UBUNTU" || exit 1
sync_to_ubuntu "$TEMP_DIR/dnsmasq.d/" "/etc/dnsmasq.d" "$OTHER_UBUNTU" "dossier dnsmasq.d vers $OTHER_UBUNTU" || exit 1
# Reconstruction de la base gravity sur les deux serveurs
manage_pihole_service "localhost" "rebuild" "Reconstruction de la base gravity"
manage_pihole_service "$OTHER_UBUNTU" "rebuild" "Reconstruction de la base gravity distante"
# Démarrage des services
manage_pihole_service "localhost" "start" "Démarrage du service Pi-hole"
manage_pihole_service "$OTHER_UBUNTU" "start" "Démarrage du service Pi-hole distant"
fi
# Nettoyage
rm -rf "$TEMP_DIR"
log "Script terminé avec succès!"
[[Category:Documentation]]
[[Category:PiHole]]
[[Category:Scripts]]