commit
22878ad608
8 changed files with 1588 additions and 19 deletions
105
.forgejo/workflows/test.yml
Normal file
105
.forgejo/workflows/test.yml
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
name: Tests et Vérifications
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, 2.1 ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests unitaires BATS
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout du code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installation de BATS
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y bats
|
||||||
|
|
||||||
|
- name: Afficher version BATS
|
||||||
|
run: bats --version
|
||||||
|
|
||||||
|
- name: Exécuter tests unitaires
|
||||||
|
run: |
|
||||||
|
cd tests
|
||||||
|
bats *.bats
|
||||||
|
|
||||||
|
- name: Upload résultats des tests
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: tests/*.log
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
shellcheck:
|
||||||
|
name: Vérification ShellCheck
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout du code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Installation de ShellCheck
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y shellcheck
|
||||||
|
|
||||||
|
- name: Afficher version ShellCheck
|
||||||
|
run: shellcheck --version
|
||||||
|
|
||||||
|
- name: Vérifier le script principal
|
||||||
|
run: |
|
||||||
|
shellcheck -x zfs-nfs-replica.sh || true
|
||||||
|
|
||||||
|
- name: Vérifier les scripts de test
|
||||||
|
run: |
|
||||||
|
shellcheck -x tests/*.bash || true
|
||||||
|
|
||||||
|
syntax:
|
||||||
|
name: Vérification syntaxe Bash
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout du code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Vérifier syntaxe du script principal
|
||||||
|
run: |
|
||||||
|
bash -n zfs-nfs-replica.sh
|
||||||
|
|
||||||
|
- name: Vérifier syntaxe des helpers de test
|
||||||
|
run: |
|
||||||
|
bash -n tests/test_helper.bash
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: Résumé des tests
|
||||||
|
runs-on: self-hosted
|
||||||
|
needs: [tests, shellcheck, syntax]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Afficher résumé
|
||||||
|
run: |
|
||||||
|
echo "=========================================="
|
||||||
|
echo "RÉSUMÉ DES TESTS"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Tests unitaires: ${{ needs.tests.result }}"
|
||||||
|
echo "ShellCheck: ${{ needs.shellcheck.result }}"
|
||||||
|
echo "Syntaxe Bash: ${{ needs.syntax.result }}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "${{ needs.tests.result }}" == "success" ]] && \
|
||||||
|
[[ "${{ needs.shellcheck.result }}" == "success" ]] && \
|
||||||
|
[[ "${{ needs.syntax.result }}" == "success" ]]; then
|
||||||
|
echo "✓ Tous les tests sont passés avec succès"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "✗ Certains tests ont échoué"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
9
tests/fixtures/zfs_list_snapshots.txt
vendored
Normal file
9
tests/fixtures/zfs_list_snapshots.txt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
NAME USED AVAIL REFER MOUNTPOINT
|
||||||
|
zpool1 5.12T 2.55T 192K /zpool1
|
||||||
|
zpool1/data-nfs-share 4.89T 2.55T 4.89T /zpool1/data-nfs-share
|
||||||
|
zpool1/data-nfs-share@autosnap_2024-12-29_14:00:00 128M - 4.89T -
|
||||||
|
zpool1/data-nfs-share@autosnap_2024-12-29_14:15:00 256M - 4.89T -
|
||||||
|
zpool1/data-nfs-share@autosnap_2024-12-29_14:30:00 64M - 4.89T -
|
||||||
|
zpool1/pbs-backups 230G 2.55T 230G /zpool1/pbs-backups
|
||||||
|
zpool1/pbs-backups@autosnap_2024-12-29_14:00:00 10M - 230G -
|
||||||
|
zpool1/pbs-backups@autosnap_2024-12-29_14:15:00 15M - 230G -
|
||||||
15
tests/fixtures/zpool_status_degraded.txt
vendored
Normal file
15
tests/fixtures/zpool_status_degraded.txt
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
pool: zpool1
|
||||||
|
state: DEGRADED
|
||||||
|
status: One or more devices has been removed by the administrator.
|
||||||
|
Sufficient replicas exist for the pool to continue functioning in a
|
||||||
|
degraded state.
|
||||||
|
action: Online the device using 'zpool online' or replace the device with
|
||||||
|
'zpool replace'.
|
||||||
|
scan: scrub repaired 0B in 0 days 02:15:32 with 0 errors on Sun Dec 15 02:39:32 2024
|
||||||
|
config:
|
||||||
|
|
||||||
|
NAME STATE READ WRITE CKSUM
|
||||||
|
zpool1 DEGRADED 0 0 0
|
||||||
|
/dev/disk/by-id/wwn-0x5000cca2dfe2e414 UNAVAIL 0 0 0
|
||||||
|
|
||||||
|
errors: No known data errors
|
||||||
10
tests/fixtures/zpool_status_healthy.txt
vendored
Normal file
10
tests/fixtures/zpool_status_healthy.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
pool: zpool1
|
||||||
|
state: ONLINE
|
||||||
|
scan: scrub repaired 0B in 0 days 02:15:32 with 0 errors on Sun Dec 15 02:39:32 2024
|
||||||
|
config:
|
||||||
|
|
||||||
|
NAME STATE READ WRITE CKSUM
|
||||||
|
zpool1 ONLINE 0 0 0
|
||||||
|
/dev/disk/by-id/wwn-0x5000cca2dfe2e414 ONLINE 0 0 0
|
||||||
|
|
||||||
|
errors: No known data errors
|
||||||
300
tests/test_health_checks.bats
Normal file
300
tests/test_health_checks.bats
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
#
|
||||||
|
# Tests unitaires pour les fonctions de health check
|
||||||
|
# Test des vérifications de santé des disques et pools ZFS
|
||||||
|
#
|
||||||
|
|
||||||
|
load test_helper
|
||||||
|
|
||||||
|
# Charger uniquement les fonctions du script (pas le code principal)
|
||||||
|
setup() {
|
||||||
|
# Setup environnement
|
||||||
|
setup_script_env
|
||||||
|
|
||||||
|
# Sourcer le script en mode test (le BATS_TEST_MODE évite l'exécution du main)
|
||||||
|
export BATS_TEST_MODE=true
|
||||||
|
source "${BATS_TEST_DIRNAME}/../zfs-nfs-replica.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
cleanup_script_env
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: get_pool_disk_uuids()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "get_pool_disk_uuids: retourne des UUIDs pour un pool sain" {
|
||||||
|
run get_pool_disk_uuids "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "wwn-0x5000cca2dfe2e414" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_pool_disk_uuids: retourne vide pour pool inexistant" {
|
||||||
|
# Mock zpool pour retourner une erreur
|
||||||
|
zpool() {
|
||||||
|
if [[ "$1" == "status" ]]; then
|
||||||
|
echo "cannot open 'fakerpool': no such pool" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
export -f zpool
|
||||||
|
|
||||||
|
run get_pool_disk_uuids "fakerpool"
|
||||||
|
|
||||||
|
# La fonction doit gérer l'erreur gracieusement
|
||||||
|
[ "$status" -ne 0 ] || [ -z "$output" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: init_disk_tracking()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "init_disk_tracking: crée le fichier d'état avec UUIDs" {
|
||||||
|
run init_disk_tracking "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -f "${STATE_DIR}/disk-uuids-zpool1.txt" ]
|
||||||
|
|
||||||
|
# Vérifier le contenu
|
||||||
|
grep -q "initialized=true" "${STATE_DIR}/disk-uuids-zpool1.txt"
|
||||||
|
grep -q "pool=zpool1" "${STATE_DIR}/disk-uuids-zpool1.txt"
|
||||||
|
grep -q "wwn-0x" "${STATE_DIR}/disk-uuids-zpool1.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "init_disk_tracking: ne réinitialise pas si déjà initialisé" {
|
||||||
|
# Créer un fichier déjà initialisé
|
||||||
|
create_disk_uuid_file "zpool1"
|
||||||
|
|
||||||
|
# Modifier le timestamp pour vérifier qu'il ne change pas
|
||||||
|
original_content=$(cat "${STATE_DIR}/disk-uuids-zpool1.txt")
|
||||||
|
|
||||||
|
run init_disk_tracking "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Le fichier ne doit pas avoir changé
|
||||||
|
new_content=$(cat "${STATE_DIR}/disk-uuids-zpool1.txt")
|
||||||
|
[ "$original_content" == "$new_content" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: verify_disk_presence()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "verify_disk_presence: succès si tous les disques présents" {
|
||||||
|
create_disk_uuid_file "zpool1" "wwn-0x5000cca2dfe2e414"
|
||||||
|
export TEST_DISK_PRESENT=true
|
||||||
|
|
||||||
|
run verify_disk_presence "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "verify_disk_presence: échec si disque manquant" {
|
||||||
|
# Créer un fichier avec UUID fictif
|
||||||
|
create_disk_uuid_file "zpool1" "wwn-0xFAKE_MISSING_DISK"
|
||||||
|
export TEST_DISK_PRESENT=false
|
||||||
|
|
||||||
|
run verify_disk_presence "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" =~ "manquant" ]] || [[ "$output" =~ "MISSING" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "verify_disk_presence: retourne erreur si fichier d'état absent" {
|
||||||
|
# Pas de fichier disk-uuids
|
||||||
|
rm -f "${STATE_DIR}/disk-uuids-zpool1.txt"
|
||||||
|
|
||||||
|
run verify_disk_presence "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: check_pool_health_status()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "check_pool_health_status: succès pour pool ONLINE avec espace libre" {
|
||||||
|
export TEST_POOL_STATE="ONLINE"
|
||||||
|
export TEST_POOL_CAPACITY=67
|
||||||
|
|
||||||
|
run check_pool_health_status "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_pool_health_status: échec pour pool DEGRADED" {
|
||||||
|
export TEST_POOL_STATE="DEGRADED"
|
||||||
|
export TEST_POOL_CAPACITY=67
|
||||||
|
|
||||||
|
run check_pool_health_status "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_pool_health_status: échec si espace disque critique (>95%)" {
|
||||||
|
export TEST_POOL_STATE="ONLINE"
|
||||||
|
export TEST_POOL_CAPACITY=96
|
||||||
|
|
||||||
|
run check_pool_health_status "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" =~ "espace libre" ]] || [[ "$output" =~ "capacity" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_pool_health_status: succès avec exactement 95% (limite)" {
|
||||||
|
export TEST_POOL_STATE="ONLINE"
|
||||||
|
export TEST_POOL_CAPACITY=95
|
||||||
|
|
||||||
|
run check_pool_health_status "zpool1"
|
||||||
|
|
||||||
|
# 95% = 5% libre, c'est la limite, doit passer
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: triple_health_check()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "triple_health_check: succès si 3/3 tentatives réussissent" {
|
||||||
|
create_disk_uuid_file "zpool1"
|
||||||
|
export TEST_POOL_STATE="ONLINE"
|
||||||
|
export TEST_POOL_CAPACITY=67
|
||||||
|
export TEST_DISK_PRESENT=true
|
||||||
|
export CHECK_DELAY=0 # Pas de délai dans tests
|
||||||
|
|
||||||
|
run triple_health_check "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "triple_health_check: échec si les 3 tentatives échouent" {
|
||||||
|
create_disk_uuid_file "zpool1" "wwn-0xFAKE_MISSING"
|
||||||
|
export TEST_DISK_PRESENT=false
|
||||||
|
export CHECK_DELAY=0
|
||||||
|
|
||||||
|
run triple_health_check "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "triple_health_check: fait vraiment 3 tentatives (pas d'early return)" {
|
||||||
|
create_disk_uuid_file "zpool1"
|
||||||
|
export TEST_POOL_STATE="DEGRADED"
|
||||||
|
export TEST_DISK_PRESENT=true
|
||||||
|
export CHECK_DELAY=0
|
||||||
|
|
||||||
|
run triple_health_check "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
|
||||||
|
# Vérifier qu'il y a bien 3 lignes d'erreur (3 tentatives)
|
||||||
|
attempt_count=$(echo "$output" | grep -c "Vérification santé #" || echo "0")
|
||||||
|
[ "$attempt_count" -eq 3 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: check_recent_critical_error()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "check_recent_critical_error: retourne 0 si erreur récente (<1h)" {
|
||||||
|
# Erreur il y a 30 minutes (1800 secondes)
|
||||||
|
local current_epoch=1735481400
|
||||||
|
local error_epoch=$((current_epoch - 1800))
|
||||||
|
|
||||||
|
export TEST_CURRENT_EPOCH=$current_epoch
|
||||||
|
create_critical_error_file "zpool1" "$error_epoch"
|
||||||
|
|
||||||
|
run check_recent_critical_error "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_recent_critical_error: retourne 1 si erreur ancienne (>1h)" {
|
||||||
|
# Erreur il y a 2 heures (7200 secondes)
|
||||||
|
local current_epoch=1735481400
|
||||||
|
local error_epoch=$((current_epoch - 7200))
|
||||||
|
|
||||||
|
export TEST_CURRENT_EPOCH=$current_epoch
|
||||||
|
create_critical_error_file "zpool1" "$error_epoch"
|
||||||
|
|
||||||
|
run check_recent_critical_error "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_recent_critical_error: retourne 1 si pas de fichier d'erreur" {
|
||||||
|
rm -f "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
|
||||||
|
run check_recent_critical_error "zpool1"
|
||||||
|
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: record_critical_error()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "record_critical_error: crée fichier avec toutes les infos" {
|
||||||
|
run record_critical_error "zpool1" "Test failure reason" "lxc_migrated"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -f "${STATE_DIR}/critical-errors-zpool1.txt" ]
|
||||||
|
|
||||||
|
grep -q "reason=Test failure reason" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
grep -q "action=lxc_migrated" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
grep -q "epoch=" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "record_critical_error: écrase le fichier précédent" {
|
||||||
|
# Créer une première erreur
|
||||||
|
create_critical_error_file "zpool1" "1735400000"
|
||||||
|
|
||||||
|
# Enregistrer une nouvelle erreur
|
||||||
|
run record_critical_error "zpool1" "New error" "lxc_stopped"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Vérifier que c'est la nouvelle erreur
|
||||||
|
grep -q "reason=New error" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
grep -q "action=lxc_stopped" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: handle_health_failure()
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "handle_health_failure: migre le LXC si première erreur" {
|
||||||
|
# Pas d'erreur récente
|
||||||
|
rm -f "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
|
||||||
|
export REMOTE_NODE_NAME="acemagician"
|
||||||
|
|
||||||
|
run handle_health_failure "zpool1" "Disk failure"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "MIGRATION" ]] || [[ "$output" =~ "migrate" ]]
|
||||||
|
|
||||||
|
# Vérifier que l'erreur a été enregistrée
|
||||||
|
[ -f "${STATE_DIR}/critical-errors-zpool1.txt" ]
|
||||||
|
grep -q "action=lxc_migrated" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "handle_health_failure: arrête le LXC si erreur récente (<1h)" {
|
||||||
|
# Erreur récente (30 min)
|
||||||
|
local current_epoch=1735481400
|
||||||
|
local error_epoch=$((current_epoch - 1800))
|
||||||
|
|
||||||
|
export TEST_CURRENT_EPOCH=$current_epoch
|
||||||
|
create_critical_error_file "zpool1" "$error_epoch"
|
||||||
|
|
||||||
|
run handle_health_failure "zpool1" "Another disk failure"
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" =~ "ARRÊT" ]] || [[ "$output" =~ "stop" ]] || [[ "$output" =~ "ping-pong" ]]
|
||||||
|
|
||||||
|
# Vérifier que l'erreur a été mise à jour
|
||||||
|
grep -q "action=lxc_stopped" "${STATE_DIR}/critical-errors-zpool1.txt"
|
||||||
|
}
|
||||||
261
tests/test_helper.bash
Normal file
261
tests/test_helper.bash
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Helpers et mocks pour les tests BATS
|
||||||
|
# Ce fichier fournit des simulations de toutes les commandes système
|
||||||
|
# utilisées par zfs-nfs-replica.sh, permettant de tester sans ZFS réel
|
||||||
|
#
|
||||||
|
|
||||||
|
# Variables globales pour les tests
|
||||||
|
export TEST_FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures"
|
||||||
|
export TEST_POOL_STATE="${TEST_POOL_STATE:-ONLINE}"
|
||||||
|
export TEST_POOL_CAPACITY="${TEST_POOL_CAPACITY:-67}"
|
||||||
|
export TEST_LXC_STATUS="${TEST_LXC_STATUS:-running}"
|
||||||
|
export TEST_DISK_PRESENT="${TEST_DISK_PRESENT:-true}"
|
||||||
|
|
||||||
|
# Mock: zpool - Simuler les commandes ZFS pool
|
||||||
|
zpool() {
|
||||||
|
case "$1" in
|
||||||
|
status)
|
||||||
|
if [[ "$2" == "-P" ]]; then
|
||||||
|
# Format détaillé avec chemins physiques
|
||||||
|
if [[ "$TEST_POOL_STATE" == "ONLINE" ]]; then
|
||||||
|
cat "${TEST_FIXTURES_DIR}/zpool_status_healthy.txt"
|
||||||
|
else
|
||||||
|
cat "${TEST_FIXTURES_DIR}/zpool_status_degraded.txt"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Format simple
|
||||||
|
echo "zpool1 ${TEST_POOL_STATE} - - -"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
local format="${4:-name,health}"
|
||||||
|
if [[ "$3" == "-o" ]]; then
|
||||||
|
case "$4" in
|
||||||
|
health)
|
||||||
|
echo "${TEST_POOL_STATE}"
|
||||||
|
;;
|
||||||
|
capacity)
|
||||||
|
echo "${TEST_POOL_CAPACITY}%"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "zpool1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "zpool1 7.67T 5.12T 2.55T ${TEST_POOL_CAPACITY}% ${TEST_POOL_STATE} -"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
import)
|
||||||
|
echo "pool zpool1 imported"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Mock zpool: commande non supportée: $*" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: zfs - Simuler les commandes ZFS dataset
|
||||||
|
zfs() {
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
if [[ "$2" == "-t" && "$3" == "snapshot" ]]; then
|
||||||
|
cat "${TEST_FIXTURES_DIR}/zfs_list_snapshots.txt"
|
||||||
|
else
|
||||||
|
cat "${TEST_FIXTURES_DIR}/zfs_list_snapshots.txt"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
get)
|
||||||
|
echo "5120000000000" # 5.12TB en bytes
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Mock zfs: commande non supportée: $*" >&2
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: pct - Simuler les commandes Proxmox LXC
|
||||||
|
pct() {
|
||||||
|
case "$1" in
|
||||||
|
status)
|
||||||
|
echo "status: ${TEST_LXC_STATUS}"
|
||||||
|
;;
|
||||||
|
exec)
|
||||||
|
# Simuler une exécution réussie dans le container
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo "Stopping CT ${2}"
|
||||||
|
TEST_LXC_STATUS="stopped"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
echo "Starting CT ${2}"
|
||||||
|
TEST_LXC_STATUS="running"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Mock pct: commande non supportée: $*" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: ha-manager - Simuler Proxmox HA manager
|
||||||
|
ha-manager() {
|
||||||
|
case "$1" in
|
||||||
|
migrate)
|
||||||
|
echo "Migrating ${2} to ${3}"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
echo "ct:103 started elitedesk"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Mock ha-manager: commande non supportée: $*" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: ssh - Simuler les connexions SSH
|
||||||
|
ssh() {
|
||||||
|
# Ignorer les options SSH (-i, -o, etc.)
|
||||||
|
local cmd=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ ! "$arg" =~ ^- ]] && [[ "$arg" != *"@"* ]] && [[ "$arg" != "root" ]]; then
|
||||||
|
cmd="$arg"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$cmd" == "echo OK" ]] || [[ "$cmd" == *"echo"* ]]; then
|
||||||
|
echo "OK"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Simuler les commandes distantes
|
||||||
|
eval "$cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: syncoid - Simuler la réplication Syncoid
|
||||||
|
syncoid() {
|
||||||
|
echo "Sending incremental zpool1@autosnap_2024-12-29_14:30:00"
|
||||||
|
echo "2.15GB 0:00:45 [48.9MB/s]"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: logger - Simuler syslog
|
||||||
|
logger() {
|
||||||
|
# Silencieux pour les tests, sauf si DEBUG
|
||||||
|
if [[ "${BATS_TEST_DEBUG:-}" == "true" ]]; then
|
||||||
|
echo "[SYSLOG] $*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: hostname - Retourner un hostname de test
|
||||||
|
hostname() {
|
||||||
|
echo "${TEST_HOSTNAME:-elitedesk}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: readlink - Simuler la résolution de symlinks
|
||||||
|
readlink() {
|
||||||
|
if [[ "$1" == "-f" ]]; then
|
||||||
|
# Retourner un chemin /dev/sdX simulé
|
||||||
|
echo "/dev/sda1"
|
||||||
|
else
|
||||||
|
echo "/dev/disk/by-id/wwn-0x5000cca2dfe2e414"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: ls pour /dev/disk/by-id/
|
||||||
|
ls() {
|
||||||
|
if [[ "$*" =~ /dev/disk/by-id ]]; then
|
||||||
|
if [[ "$TEST_DISK_PRESENT" == "true" ]]; then
|
||||||
|
echo "lrwxrwxrwx 1 root root 9 Dec 29 14:00 wwn-0x5000cca2dfe2e414 -> ../../sda1"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Appeler le vrai ls pour autres cas
|
||||||
|
command ls "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock: date - Contrôler le temps dans les tests
|
||||||
|
date() {
|
||||||
|
if [[ "$1" == "+%s" ]]; then
|
||||||
|
echo "${TEST_CURRENT_EPOCH:-1735481400}"
|
||||||
|
elif [[ "$1" == "+%Y-%m-%d_%H:%M:%S" ]]; then
|
||||||
|
echo "2024-12-29_14:30:00"
|
||||||
|
else
|
||||||
|
command date "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Setup des variables d'environnement pour le script
|
||||||
|
setup_script_env() {
|
||||||
|
export ZPOOLS=("zpool1")
|
||||||
|
export CTID=103
|
||||||
|
export CONTAINER_NAME="nfs-server"
|
||||||
|
export STATE_DIR="${BATS_TMPDIR}/zfs-nfs-replica"
|
||||||
|
export LOG_DIR="${BATS_TMPDIR}/logs"
|
||||||
|
export HEALTH_CHECK_MIN_FREE_SPACE=5
|
||||||
|
export HEALTH_CHECK_ERROR_COOLDOWN=3600
|
||||||
|
export NOTIFICATION_ENABLED=false
|
||||||
|
export AUTO_UPDATE_ENABLED=false
|
||||||
|
export CHECK_DELAY=0 # Pas de délai dans les tests
|
||||||
|
|
||||||
|
# Cluster nodes
|
||||||
|
declare -gA CLUSTER_NODES=(
|
||||||
|
["acemagician"]="192.168.100.10"
|
||||||
|
["elitedesk"]="192.168.100.20"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Créer les répertoires nécessaires
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Nettoyer l'environnement de test
|
||||||
|
cleanup_script_env() {
|
||||||
|
rm -rf "${BATS_TMPDIR}/zfs-nfs-replica"
|
||||||
|
rm -rf "${BATS_TMPDIR}/logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Créer un fichier d'état disk-uuids
|
||||||
|
create_disk_uuid_file() {
|
||||||
|
local pool="$1"
|
||||||
|
local uuid="${2:-wwn-0x5000cca2dfe2e414}"
|
||||||
|
|
||||||
|
cat > "${STATE_DIR}/disk-uuids-${pool}.txt" <<EOF
|
||||||
|
initialized=true
|
||||||
|
timestamp=2024-12-29_14:00:00
|
||||||
|
hostname=elitedesk
|
||||||
|
pool=${pool}
|
||||||
|
# Physical disk UUIDs
|
||||||
|
${uuid}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Créer un fichier d'erreur critique
|
||||||
|
create_critical_error_file() {
|
||||||
|
local pool="$1"
|
||||||
|
local epoch="${2:-${TEST_CURRENT_EPOCH:-1735481400}}"
|
||||||
|
|
||||||
|
cat > "${STATE_DIR}/critical-errors-${pool}.txt" <<EOF
|
||||||
|
timestamp=2024-12-29_14:00:00
|
||||||
|
epoch=${epoch}
|
||||||
|
reason=Test error
|
||||||
|
action=lxc_migrated
|
||||||
|
target_node=acemagician
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exporter tous les mocks
|
||||||
|
export -f zpool zfs pct ha-manager ssh syncoid logger hostname readlink ls date
|
||||||
|
export -f setup_script_env cleanup_script_env
|
||||||
|
export -f create_disk_uuid_file create_critical_error_file
|
||||||
170
tests/test_node_config.bats
Normal file
170
tests/test_node_config.bats
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
#
|
||||||
|
# Tests unitaires pour la configuration des nœuds
|
||||||
|
# Test de la logique de découverte du nœud distant
|
||||||
|
#
|
||||||
|
|
||||||
|
load test_helper
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
setup_script_env
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
cleanup_script_env
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: Configuration des nœuds avec CLUSTER_NODES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "CLUSTER_NODES: contient acemagician et elitedesk" {
|
||||||
|
# Vérifier que le tableau associatif est bien défini
|
||||||
|
[ -n "${CLUSTER_NODES[acemagician]}" ]
|
||||||
|
[ -n "${CLUSTER_NODES[elitedesk]}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "CLUSTER_NODES: IPs correctes pour chaque nœud" {
|
||||||
|
[ "${CLUSTER_NODES[acemagician]}" = "192.168.100.10" ]
|
||||||
|
[ "${CLUSTER_NODES[elitedesk]}" = "192.168.100.20" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: Détection du nœud distant
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "Nœud distant: elitedesk détecte acemagician" {
|
||||||
|
export TEST_HOSTNAME="elitedesk"
|
||||||
|
LOCAL_NODE=$(hostname)
|
||||||
|
|
||||||
|
# Trouver le nœud distant
|
||||||
|
REMOTE_NODE_NAME=""
|
||||||
|
REMOTE_NODE_IP=""
|
||||||
|
for node in "${!CLUSTER_NODES[@]}"; do
|
||||||
|
if [[ "$node" != "$LOCAL_NODE" ]]; then
|
||||||
|
REMOTE_NODE_NAME="$node"
|
||||||
|
REMOTE_NODE_IP="${CLUSTER_NODES[$node]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$REMOTE_NODE_NAME" = "acemagician" ]
|
||||||
|
[ "$REMOTE_NODE_IP" = "192.168.100.10" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Nœud distant: acemagician détecte elitedesk" {
|
||||||
|
export TEST_HOSTNAME="acemagician"
|
||||||
|
LOCAL_NODE=$(hostname)
|
||||||
|
|
||||||
|
# Trouver le nœud distant
|
||||||
|
REMOTE_NODE_NAME=""
|
||||||
|
REMOTE_NODE_IP=""
|
||||||
|
for node in "${!CLUSTER_NODES[@]}"; do
|
||||||
|
if [[ "$node" != "$LOCAL_NODE" ]]; then
|
||||||
|
REMOTE_NODE_NAME="$node"
|
||||||
|
REMOTE_NODE_IP="${CLUSTER_NODES[$node]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$REMOTE_NODE_NAME" = "elitedesk" ]
|
||||||
|
[ "$REMOTE_NODE_IP" = "192.168.100.20" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Nœud distant: erreur si nœud local inconnu" {
|
||||||
|
export TEST_HOSTNAME="unknown-node"
|
||||||
|
LOCAL_NODE=$(hostname)
|
||||||
|
|
||||||
|
# Vérifier que le nœud local n'est pas dans la config
|
||||||
|
if [[ ! -v "CLUSTER_NODES[$LOCAL_NODE]" ]]; then
|
||||||
|
# Comportement attendu : erreur
|
||||||
|
run echo "Node not found"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
else
|
||||||
|
# Ne devrait pas arriver ici
|
||||||
|
false
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Nœud distant: erreur si cluster à 1 seul nœud" {
|
||||||
|
# Créer un cluster avec un seul nœud
|
||||||
|
declare -A TEST_CLUSTER=(
|
||||||
|
["lonely-node"]="192.168.100.99"
|
||||||
|
)
|
||||||
|
|
||||||
|
export TEST_HOSTNAME="lonely-node"
|
||||||
|
LOCAL_NODE=$(hostname)
|
||||||
|
|
||||||
|
# Chercher nœud distant
|
||||||
|
REMOTE_NODE_NAME=""
|
||||||
|
REMOTE_NODE_IP=""
|
||||||
|
for node in "${!TEST_CLUSTER[@]}"; do
|
||||||
|
if [[ "$node" != "$LOCAL_NODE" ]]; then
|
||||||
|
REMOTE_NODE_NAME="$node"
|
||||||
|
REMOTE_NODE_IP="${TEST_CLUSTER[$node]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Aucun nœud distant trouvé
|
||||||
|
[ -z "$REMOTE_NODE_NAME" ]
|
||||||
|
[ -z "$REMOTE_NODE_IP" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: Extension à 3+ nœuds
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "Cluster 3 nœuds: détecte le premier nœud distant disponible" {
|
||||||
|
# Créer un cluster avec 3 nœuds
|
||||||
|
declare -A EXTENDED_CLUSTER=(
|
||||||
|
["node1"]="192.168.100.10"
|
||||||
|
["node2"]="192.168.100.20"
|
||||||
|
["node3"]="192.168.100.30"
|
||||||
|
)
|
||||||
|
|
||||||
|
export TEST_HOSTNAME="node1"
|
||||||
|
LOCAL_NODE=$(hostname)
|
||||||
|
|
||||||
|
# Trouver le premier nœud distant
|
||||||
|
REMOTE_NODE_NAME=""
|
||||||
|
REMOTE_NODE_IP=""
|
||||||
|
for node in "${!EXTENDED_CLUSTER[@]}"; do
|
||||||
|
if [[ "$node" != "$LOCAL_NODE" ]]; then
|
||||||
|
REMOTE_NODE_NAME="$node"
|
||||||
|
REMOTE_NODE_IP="${EXTENDED_CLUSTER[$node]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Un nœud distant doit être trouvé (node2 ou node3)
|
||||||
|
[ -n "$REMOTE_NODE_NAME" ]
|
||||||
|
[ -n "$REMOTE_NODE_IP" ]
|
||||||
|
[[ "$REMOTE_NODE_NAME" != "node1" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tests: Validation des variables de configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@test "Variables de config: ZPOOLS est un tableau non vide" {
|
||||||
|
[ "${#ZPOOLS[@]}" -gt 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Variables de config: CTID est défini" {
|
||||||
|
[ -n "$CTID" ]
|
||||||
|
[ "$CTID" -eq "$CTID" ] 2>/dev/null # Vérifier que c'est un nombre
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Variables de config: CONTAINER_NAME est défini" {
|
||||||
|
[ -n "$CONTAINER_NAME" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Variables de config: HEALTH_CHECK_MIN_FREE_SPACE valide" {
|
||||||
|
[ "$HEALTH_CHECK_MIN_FREE_SPACE" -ge 0 ]
|
||||||
|
[ "$HEALTH_CHECK_MIN_FREE_SPACE" -le 100 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "Variables de config: HEALTH_CHECK_ERROR_COOLDOWN valide" {
|
||||||
|
[ "$HEALTH_CHECK_ERROR_COOLDOWN" -gt 0 ]
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,36 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Script de réplication ZFS automatique pour NFS HA (Multi-pools)
|
# Script de réplication ZFS automatique pour NFS HA (Multi-pools)
|
||||||
# À déployer sur acemagician et elitedesk
|
# À déployer sur tous les nœuds de production du cluster Proxmox
|
||||||
#
|
#
|
||||||
# Ce script version 2.0 :
|
# Ce script version 2.1 :
|
||||||
# - Supporte la réplication de plusieurs pools ZFS simultanément
|
# - Supporte la réplication de plusieurs pools ZFS simultanément
|
||||||
# - Vérifie 3 fois que le LXC nfs-server est actif localement
|
# - Vérifie 3 fois que le LXC nfs-server est actif localement
|
||||||
|
# - Vérifie l'état de santé des disques et pools ZFS avant réplication
|
||||||
|
# - Détecte les disques manquants, pools dégradés, erreurs I/O
|
||||||
|
# - Migration automatique du LXC en cas de défaillance matérielle
|
||||||
|
# - Protection anti-ping-pong (arrêt du LXC si erreur < 1h)
|
||||||
# - Détermine le nœud distant automatiquement
|
# - Détermine le nœud distant automatiquement
|
||||||
# - Réplique chaque pool ZFS vers le nœud passif avec isolation des erreurs
|
# - Réplique chaque pool ZFS vers le nœud passif avec isolation des erreurs
|
||||||
# - Utilise un verrou par pool pour éviter les réplications concurrentes
|
# - Utilise un verrou par pool pour éviter les réplications concurrentes
|
||||||
# - Gère l'activation/désactivation de Sanoid selon le nœud actif
|
# - Gère l'activation/désactivation de Sanoid selon le nœud actif
|
||||||
# - Logs avec rotation automatique (2 semaines de rétention)
|
# - Logs avec rotation automatique (2 semaines de rétention)
|
||||||
# - Fichiers d'état séparés par pool
|
# - Fichiers d'état séparés par pool (tailles, UUIDs disques, erreurs critiques)
|
||||||
#
|
#
|
||||||
# Auteur : BENE Maël
|
# Auteur : BENE Maël
|
||||||
# Version : 2.0.1
|
# Version : 2.1.0
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
SCRIPT_VERSION="2.0.1"
|
SCRIPT_VERSION="2.1.0"
|
||||||
REPO_URL="https://forgejo.tellserv.fr/Tellsanguis/zfs-sync-nfs-ha"
|
REPO_URL="https://forgejo.tellserv.fr/Tellsanguis/zfs-sync-nfs-ha"
|
||||||
SCRIPT_URL="${REPO_URL}/raw/branch/main/zfs-nfs-replica.sh"
|
SCRIPT_URL="${REPO_URL}/raw/branch/main/zfs-nfs-replica.sh"
|
||||||
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
||||||
AUTO_UPDATE_ENABLED=true # Mettre à false pour désactiver l'auto-update
|
AUTO_UPDATE_ENABLED=true # Mettre à false pour désactiver l'auto-update
|
||||||
|
|
||||||
|
# Configuration du container LXC
|
||||||
CTID=103
|
CTID=103
|
||||||
CONTAINER_NAME="nfs-server"
|
CONTAINER_NAME="nfs-server"
|
||||||
|
|
||||||
|
|
@ -33,6 +38,14 @@ CONTAINER_NAME="nfs-server"
|
||||||
# Ajouter ou retirer des pools selon vos besoins
|
# Ajouter ou retirer des pools selon vos besoins
|
||||||
ZPOOLS=("zpool1" "zpool2")
|
ZPOOLS=("zpool1" "zpool2")
|
||||||
|
|
||||||
|
# Configuration des nœuds du cluster
|
||||||
|
# Format: NODE_NAME:IP_ADDRESS
|
||||||
|
# Ajouter tous les nœuds de production du cluster
|
||||||
|
declare -A CLUSTER_NODES=(
|
||||||
|
["acemagician"]="192.168.100.10"
|
||||||
|
["elitedesk"]="192.168.100.20"
|
||||||
|
)
|
||||||
|
|
||||||
CHECK_DELAY=2 # Délai entre chaque vérification (secondes)
|
CHECK_DELAY=2 # Délai entre chaque vérification (secondes)
|
||||||
LOG_FACILITY="local0"
|
LOG_FACILITY="local0"
|
||||||
SSH_KEY="/root/.ssh/id_ed25519_zfs_replication"
|
SSH_KEY="/root/.ssh/id_ed25519_zfs_replication"
|
||||||
|
|
@ -44,6 +57,33 @@ MIN_REMOTE_RATIO=50 # Le distant doit avoir au moins 50% de la taille du local
|
||||||
LOG_DIR="/var/log/zfs-nfs-replica"
|
LOG_DIR="/var/log/zfs-nfs-replica"
|
||||||
LOG_RETENTION_DAYS=14
|
LOG_RETENTION_DAYS=14
|
||||||
|
|
||||||
|
# Configuration de vérification de santé des pools
|
||||||
|
HEALTH_CHECK_MIN_FREE_SPACE=5 # Pourcentage minimum d'espace libre
|
||||||
|
HEALTH_CHECK_ERROR_COOLDOWN=3600 # Anti-ping-pong: 1 heure en secondes
|
||||||
|
|
||||||
|
# Configuration des notifications via Apprise
|
||||||
|
NOTIFICATION_ENABLED=true # Activer/désactiver les notifications
|
||||||
|
NOTIFICATION_MODE="INFO" # "INFO" (toutes les notifs) ou "ERROR" (erreurs uniquement)
|
||||||
|
|
||||||
|
# URLs Apprise (séparées par des espaces) - Exemples:
|
||||||
|
# Discord: discord://webhook_id/webhook_token
|
||||||
|
# Telegram: tgram://bot_token/chat_id
|
||||||
|
# Gotify: gotify://hostname/token
|
||||||
|
# Email: mailto://user:pass@smtp.domain.com
|
||||||
|
# Ntfy: ntfy://topic ou ntfy://hostname/topic
|
||||||
|
# Slack: slack://TokenA/TokenB/TokenC
|
||||||
|
# Voir https://github.com/caronc/apprise pour plus de services
|
||||||
|
APPRISE_URLS="" # Configurer vos URLs ici
|
||||||
|
|
||||||
|
# Exemples d'utilisation:
|
||||||
|
# APPRISE_URLS="discord://webhook_id/token"
|
||||||
|
# APPRISE_URLS="discord://id/token gotify://server/token"
|
||||||
|
# APPRISE_URLS="mailto://user:pass@gmail.com tgram://bot_token/chat_id"
|
||||||
|
|
||||||
|
# Configuration environnement Python pour Apprise
|
||||||
|
APPRISE_VENV_DIR="${STATE_DIR}/venv" # Répertoire du virtualenv Python
|
||||||
|
APPRISE_BIN="${APPRISE_VENV_DIR}/bin/apprise" # Binaire Apprise dans le venv
|
||||||
|
|
||||||
# Initialiser le répertoire de logs
|
# Initialiser le répertoire de logs
|
||||||
init_logging() {
|
init_logging() {
|
||||||
mkdir -p "$LOG_DIR"
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
@ -87,6 +127,123 @@ log() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Initialisation de l'environnement Python pour Apprise
|
||||||
|
# Note: Le venv est persistant dans /var/lib/zfs-nfs-replica/venv
|
||||||
|
setup_apprise_venv() {
|
||||||
|
# Créer le répertoire d'état si nécessaire
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# Vérifier si le venv existe déjà
|
||||||
|
if [[ ! -d "$APPRISE_VENV_DIR" ]]; then
|
||||||
|
log "info" "Création de l'environnement Python virtuel pour Apprise..."
|
||||||
|
|
||||||
|
# Créer le virtualenv (python3 et venv sont préinstallés sur Proxmox)
|
||||||
|
if ! python3 -m venv "$APPRISE_VENV_DIR" 2>/dev/null; then
|
||||||
|
log "error" "Échec de la création du virtualenv"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "info" "✓ Virtualenv créé: ${APPRISE_VENV_DIR}"
|
||||||
|
|
||||||
|
# Installer pip dans le venv (pas installé par défaut sur Proxmox)
|
||||||
|
log "info" "Installation de pip dans le virtualenv..."
|
||||||
|
if ! "${APPRISE_VENV_DIR}/bin/python" -m ensurepip --upgrade >/dev/null 2>&1; then
|
||||||
|
log "error" "Échec de l'installation de pip"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "info" "✓ Pip installé dans le virtualenv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier si Apprise est installé dans le venv
|
||||||
|
if [[ ! -f "$APPRISE_BIN" ]]; then
|
||||||
|
log "info" "Installation d'Apprise dans le virtualenv..."
|
||||||
|
|
||||||
|
# Installer Apprise via pip du venv
|
||||||
|
if "${APPRISE_VENV_DIR}/bin/pip" install --quiet apprise 2>/dev/null; then
|
||||||
|
log "info" "✓ Apprise installé avec succès"
|
||||||
|
else
|
||||||
|
log "error" "Échec de l'installation d'Apprise"
|
||||||
|
log "error" "Essayer manuellement: ${APPRISE_VENV_DIR}/bin/pip install apprise"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier que Apprise fonctionne
|
||||||
|
if [[ -x "$APPRISE_BIN" ]]; then
|
||||||
|
local apprise_version
|
||||||
|
apprise_version=$("$APPRISE_BIN" --version 2>/dev/null | head -1)
|
||||||
|
log "info" "✓ Apprise prêt: ${apprise_version}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "Apprise installé mais non exécutable"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction d'envoi de notifications via Apprise
|
||||||
|
send_notification() {
|
||||||
|
local severity="$1" # "info" ou "error"
|
||||||
|
local title="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
# Vérifier si les notifications sont activées
|
||||||
|
if [[ "${NOTIFICATION_ENABLED}" != "true" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier si des URLs Apprise sont configurées
|
||||||
|
if [[ -z "${APPRISE_URLS}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Filtrer selon le mode de notification
|
||||||
|
if [[ "${NOTIFICATION_MODE}" == "ERROR" ]] && [[ "$severity" != "error" ]]; then
|
||||||
|
# Mode ERROR: ignorer les notifications info
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier si Apprise est installé dans le venv
|
||||||
|
if [[ ! -x "$APPRISE_BIN" ]]; then
|
||||||
|
log "warning" "Apprise non disponible - notifications désactivées"
|
||||||
|
log "warning" "Le virtualenv n'a pas été correctement initialisé"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Préparer le corps du message
|
||||||
|
local hostname
|
||||||
|
hostname=$(hostname)
|
||||||
|
local timestamp
|
||||||
|
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
local full_message="[${hostname}] [${timestamp}]
|
||||||
|
${message}
|
||||||
|
|
||||||
|
Script: zfs-nfs-replica v${SCRIPT_VERSION}
|
||||||
|
Nœud: ${hostname}"
|
||||||
|
|
||||||
|
# Déterminer le type de notification Apprise selon la sévérité
|
||||||
|
local notification_type="info"
|
||||||
|
if [[ "$severity" == "error" ]]; then
|
||||||
|
notification_type="warning" # Apprise utilise "warning" pour les erreurs critiques
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Envoyer la notification à tous les services configurés
|
||||||
|
# apprise supporte plusieurs URLs séparées par des espaces
|
||||||
|
if "$APPRISE_BIN" \
|
||||||
|
--notification-type="$notification_type" \
|
||||||
|
--title="ZFS NFS HA: ${title}" \
|
||||||
|
--body="$full_message" \
|
||||||
|
${APPRISE_URLS} \
|
||||||
|
>/dev/null 2>&1; then
|
||||||
|
log "info" "Notification envoyée avec succès"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "warning" "Échec d'envoi de la notification via Apprise"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Fonction d'auto-update
|
# Fonction d'auto-update
|
||||||
auto_update() {
|
auto_update() {
|
||||||
# Vérifier si l'auto-update est activé
|
# Vérifier si l'auto-update est activé
|
||||||
|
|
@ -318,6 +475,259 @@ check_common_snapshots() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extraction des UUIDs des disques physiques d'un pool
|
||||||
|
get_pool_disk_uuids() {
|
||||||
|
local pool="$1"
|
||||||
|
|
||||||
|
# Obtenir la configuration du pool avec chemins physiques
|
||||||
|
local pool_config
|
||||||
|
pool_config=$(zpool status -P "$pool" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$pool_config" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extraire les devices physiques (lignes contenant /dev/)
|
||||||
|
# Format typique: " /dev/sdb1 ONLINE 0 0 0"
|
||||||
|
local devices
|
||||||
|
devices=$(echo "$pool_config" | grep -E '^\s+/dev/' | awk '{print $1}')
|
||||||
|
|
||||||
|
if [[ -z "$devices" ]]; then
|
||||||
|
# Pool virtuel ou pas de disques physiques
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pour chaque device, résoudre vers /dev/disk/by-id/ (méthode optimisée)
|
||||||
|
local uuids=()
|
||||||
|
while read -r device; do
|
||||||
|
if [[ -z "$device" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Résoudre le device réel
|
||||||
|
local device_real
|
||||||
|
device_real=$(readlink -f "$device" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$device_real" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Chercher les liens dans /dev/disk/by-id/ pointant vers ce device
|
||||||
|
# Méthode optimisée: ls -l au lieu de find
|
||||||
|
local found_uuids
|
||||||
|
found_uuids=$(ls -l /dev/disk/by-id/ 2>/dev/null | \
|
||||||
|
awk -v target="$(basename "$device_real")" '$NF == target {print $(NF-2)}' | \
|
||||||
|
grep -E '^(wwn-|ata-|scsi-|nvme-)' || true)
|
||||||
|
|
||||||
|
if [[ -n "$found_uuids" ]]; then
|
||||||
|
while read -r uuid_name; do
|
||||||
|
uuids+=("$uuid_name")
|
||||||
|
done <<< "$found_uuids"
|
||||||
|
fi
|
||||||
|
done <<< "$devices"
|
||||||
|
|
||||||
|
# Retourner les UUIDs triés et uniques
|
||||||
|
if [[ ${#uuids[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${uuids[@]}" | sort -u
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
# Aucun UUID trouvé (pool virtuel possible)
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialisation du tracking des disques pour un pool
|
||||||
|
init_disk_tracking() {
|
||||||
|
local pool="$1"
|
||||||
|
local uuids_file="${STATE_DIR}/disk-uuids-${pool}.txt"
|
||||||
|
|
||||||
|
# Vérifier si déjà initialisé
|
||||||
|
if [[ -f "$uuids_file" ]] && grep -q "^initialized=true" "$uuids_file" 2>/dev/null; then
|
||||||
|
log "info" "Tracking des disques déjà initialisé pour ${pool}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "info" "Initialisation du tracking des disques pour ${pool}"
|
||||||
|
|
||||||
|
# Créer le répertoire d'état si nécessaire
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# Obtenir les UUIDs des disques du pool
|
||||||
|
local uuids
|
||||||
|
uuids=$(get_pool_disk_uuids "$pool")
|
||||||
|
|
||||||
|
if [[ -z "$uuids" ]]; then
|
||||||
|
log "warning" "Aucun disque physique détecté pour ${pool} (pool virtuel?)"
|
||||||
|
# Créer quand même un fichier pour marquer comme initialisé
|
||||||
|
cat > "$uuids_file" <<EOF
|
||||||
|
initialized=true
|
||||||
|
timestamp=$(date '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
hostname=$(hostname)
|
||||||
|
pool=${pool}
|
||||||
|
vdev_type=virtual
|
||||||
|
# Aucun disque physique détecté
|
||||||
|
EOF
|
||||||
|
chmod 600 "$uuids_file"
|
||||||
|
chown root:root "$uuids_file" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Créer le fichier de tracking
|
||||||
|
cat > "$uuids_file" <<EOF
|
||||||
|
initialized=true
|
||||||
|
timestamp=$(date '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
hostname=$(hostname)
|
||||||
|
pool=${pool}
|
||||||
|
# Physical disk UUIDs
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Ajouter chaque UUID
|
||||||
|
while read -r uuid; do
|
||||||
|
echo "$uuid" >> "$uuids_file"
|
||||||
|
log "info" "Disque détecté pour ${pool}: ${uuid}"
|
||||||
|
done <<< "$uuids"
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod 600 "$uuids_file"
|
||||||
|
chown root:root "$uuids_file" 2>/dev/null
|
||||||
|
|
||||||
|
local disk_count
|
||||||
|
disk_count=$(echo "$uuids" | wc -l)
|
||||||
|
log "info" "✓ Tracking des disques initialisé pour ${pool}: ${disk_count} disque(s) enregistré(s)"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérification de la présence des disques trackés
|
||||||
|
verify_disk_presence() {
|
||||||
|
local pool="$1"
|
||||||
|
local uuids_file="${STATE_DIR}/disk-uuids-${pool}.txt"
|
||||||
|
|
||||||
|
if [[ ! -f "$uuids_file" ]]; then
|
||||||
|
log "error" "Fichier de tracking des disques non trouvé: ${uuids_file}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lire les UUIDs du fichier (ignorer les lignes de commentaires et metadata)
|
||||||
|
local tracked_uuids
|
||||||
|
tracked_uuids=$(grep -E '^(wwn-|ata-|scsi-|nvme-)' "$uuids_file" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$tracked_uuids" ]]; then
|
||||||
|
# Pool virtuel, pas de vérification nécessaire
|
||||||
|
log "info" "Pool ${pool}: aucun disque physique à vérifier (pool virtuel)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier chaque UUID
|
||||||
|
local missing_disks=0
|
||||||
|
while read -r uuid; do
|
||||||
|
local disk_path="/dev/disk/by-id/${uuid}"
|
||||||
|
|
||||||
|
if [[ ! -L "$disk_path" ]]; then
|
||||||
|
log "error" "Disque manquant pour ${pool}: ${uuid}"
|
||||||
|
log "error" " Emplacement attendu: ${disk_path}"
|
||||||
|
missing_disks=$((missing_disks + 1))
|
||||||
|
elif [[ ! -e "$disk_path" ]]; then
|
||||||
|
log "error" "Symlink dangling pour ${pool}: ${uuid}"
|
||||||
|
log "error" " Le lien existe mais pointe vers un device inexistant"
|
||||||
|
missing_disks=$((missing_disks + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$tracked_uuids"
|
||||||
|
|
||||||
|
if [[ $missing_disks -gt 0 ]]; then
|
||||||
|
log "error" "✗ ${missing_disks} disque(s) manquant(s) pour ${pool}"
|
||||||
|
send_notification "error" "Disque(s) manquant(s) - ${pool}" \
|
||||||
|
"${missing_disks} disque(s) manquant(s) détecté(s) pour le pool ${pool}.
|
||||||
|
|
||||||
|
Vérifier les connexions USB/SATA et l'état des disques.
|
||||||
|
Une migration automatique du LXC peut être déclenchée."
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
log "info" "✓ Tous les disques présents pour ${pool}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérification de l'état de santé du pool ZFS
|
||||||
|
check_pool_health_status() {
|
||||||
|
local pool="$1"
|
||||||
|
local health_issues=0
|
||||||
|
|
||||||
|
# Check 1: Status du pool (ONLINE/DEGRADED/FAULTED)
|
||||||
|
local pool_health
|
||||||
|
pool_health=$(zpool list -H -o health "$pool" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$pool_health" ]]; then
|
||||||
|
log "error" "Impossible de récupérer le status du pool ${pool}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$pool_health" != "ONLINE" ]]; then
|
||||||
|
log "error" "Pool ${pool} en état dégradé: ${pool_health}"
|
||||||
|
send_notification "error" "Pool ZFS ${pool_health} - ${pool}" \
|
||||||
|
"Le pool ZFS ${pool} est en état ${pool_health}.
|
||||||
|
|
||||||
|
Vérifier l'état des disques avec: zpool status ${pool}
|
||||||
|
Une migration automatique du LXC peut être déclenchée."
|
||||||
|
health_issues=$((health_issues + 1))
|
||||||
|
else
|
||||||
|
log "info" "✓ Pool ${pool} status: ONLINE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2: Espace libre (doit être > HEALTH_CHECK_MIN_FREE_SPACE%)
|
||||||
|
local capacity
|
||||||
|
capacity=$(zpool list -H -o capacity "$pool" 2>/dev/null | sed 's/%//')
|
||||||
|
|
||||||
|
if [[ -n "$capacity" ]]; then
|
||||||
|
local min_capacity=$((100 - HEALTH_CHECK_MIN_FREE_SPACE))
|
||||||
|
|
||||||
|
if [[ $capacity -ge $min_capacity ]]; then
|
||||||
|
log "error" "Espace disque critique pour ${pool}: ${capacity}% utilisé (seuil: ${min_capacity}%)"
|
||||||
|
send_notification "error" "Espace disque critique - ${pool}" \
|
||||||
|
"Le pool ${pool} est presque plein: ${capacity}% utilisé.
|
||||||
|
|
||||||
|
Seuil critique: ${min_capacity}%
|
||||||
|
Espace libre restant: $((100 - capacity))%
|
||||||
|
|
||||||
|
ACTION REQUISE: Libérer de l'espace ou agrandir le pool."
|
||||||
|
health_issues=$((health_issues + 1))
|
||||||
|
else
|
||||||
|
local free_percent=$((100 - capacity))
|
||||||
|
log "info" "✓ Espace libre pour ${pool}: ${free_percent}% (seuil minimum: ${HEALTH_CHECK_MIN_FREE_SPACE}%)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: Scrub/resilver en cours (non bloquant, juste informatif)
|
||||||
|
if zpool status "$pool" 2>/dev/null | grep -qi "scrub in progress"; then
|
||||||
|
log "warning" "Scrub en cours sur ${pool} (non bloquant)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if zpool status "$pool" 2>/dev/null | grep -qi "resilver in progress"; then
|
||||||
|
log "warning" "Resilver en cours sur ${pool} (non bloquant)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: Erreurs I/O (READ/WRITE/CKSUM)
|
||||||
|
local error_lines
|
||||||
|
error_lines=$(zpool status "$pool" 2>/dev/null | grep -E "errors:" | grep -v "No known data errors")
|
||||||
|
|
||||||
|
if [[ -n "$error_lines" ]]; then
|
||||||
|
log "error" "Erreurs détectées sur le pool ${pool}:"
|
||||||
|
zpool status "$pool" 2>/dev/null | grep -E "(READ|WRITE|CKSUM)" | while read -r line; do
|
||||||
|
log "error" " ${line}"
|
||||||
|
done
|
||||||
|
health_issues=$((health_issues + 1))
|
||||||
|
else
|
||||||
|
log "info" "✓ Aucune erreur I/O détectée sur ${pool}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $health_issues -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "✗ ${health_issues} problème(s) de santé détecté(s) sur ${pool}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Récupération des tailles de datasets
|
# Récupération des tailles de datasets
|
||||||
get_dataset_sizes() {
|
get_dataset_sizes() {
|
||||||
local target="$1" # "local" ou "remote:IP"
|
local target="$1" # "local" ou "remote:IP"
|
||||||
|
|
@ -478,6 +888,224 @@ check_size_safety() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Triple vérification de santé (mirroring verify_lxc_is_active pattern)
|
||||||
|
triple_health_check() {
|
||||||
|
local pool="$1"
|
||||||
|
local success_count=0
|
||||||
|
|
||||||
|
for i in 1 2 3; do
|
||||||
|
log "info" "Vérification santé #${i}/3 pour ${pool}"
|
||||||
|
|
||||||
|
# Vérifier à la fois la présence des disques ET l'état du pool
|
||||||
|
if verify_disk_presence "$pool" && check_pool_health_status "$pool"; then
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
log "info" "Vérification santé #${i}/3 réussie pour ${pool}"
|
||||||
|
else
|
||||||
|
log "error" "Vérification santé #${i}/3 échouée pour ${pool}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Délai entre les vérifications (sauf après la dernière)
|
||||||
|
if [[ $i -lt 3 ]]; then
|
||||||
|
sleep "$CHECK_DELAY"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $success_count -eq 3 ]]; then
|
||||||
|
log "info" "✓ Triple vérification santé réussie pour ${pool} (${success_count}/3)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "✗ Triple vérification santé échouée pour ${pool}: ${success_count}/3 vérifications réussies"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérification de l'existence d'une erreur critique récente (anti-ping-pong)
|
||||||
|
check_recent_critical_error() {
|
||||||
|
local pool="$1"
|
||||||
|
local error_file="${STATE_DIR}/critical-errors-${pool}.txt"
|
||||||
|
|
||||||
|
if [[ ! -f "$error_file" ]]; then
|
||||||
|
# Pas d'erreur précédente
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lire le timestamp epoch de la dernière erreur
|
||||||
|
local last_error_epoch
|
||||||
|
last_error_epoch=$(grep "^epoch=" "$error_file" | cut -d= -f2)
|
||||||
|
|
||||||
|
if [[ -z "$last_error_epoch" ]]; then
|
||||||
|
# Fichier corrompu ou format invalide
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculer le temps écoulé depuis la dernière erreur
|
||||||
|
local current_epoch
|
||||||
|
current_epoch=$(date +%s)
|
||||||
|
local time_diff=$((current_epoch - last_error_epoch))
|
||||||
|
|
||||||
|
if [[ $time_diff -lt $HEALTH_CHECK_ERROR_COOLDOWN ]]; then
|
||||||
|
# Erreur récente (< 1 heure par défaut)
|
||||||
|
log "warning" "Erreur critique récente détectée: il y a ${time_diff}s (seuil: ${HEALTH_CHECK_ERROR_COOLDOWN}s)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
# Erreur ancienne (> 1 heure)
|
||||||
|
log "info" "Dernière erreur critique: il y a ${time_diff}s (> seuil de ${HEALTH_CHECK_ERROR_COOLDOWN}s)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enregistrement d'une erreur critique
|
||||||
|
record_critical_error() {
|
||||||
|
local pool="$1"
|
||||||
|
local reason="$2"
|
||||||
|
local action="$3" # "lxc_migrated" ou "lxc_stopped" ou "lxc_stopped_failsafe"
|
||||||
|
local error_file="${STATE_DIR}/critical-errors-${pool}.txt"
|
||||||
|
|
||||||
|
# Créer le répertoire d'état si nécessaire
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# Créer ou écraser le fichier d'erreur
|
||||||
|
cat > "$error_file" <<EOF
|
||||||
|
timestamp=$(date '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
epoch=$(date +%s)
|
||||||
|
reason=${reason}
|
||||||
|
action=${action}
|
||||||
|
target_node=${REMOTE_NODE_NAME}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Définir les permissions
|
||||||
|
chmod 600 "$error_file"
|
||||||
|
chown root:root "$error_file" 2>/dev/null
|
||||||
|
|
||||||
|
log "error" "Erreur critique enregistrée pour ${pool}: ${reason} → ${action}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gestion de l'échec de santé - Migration ou arrêt du LXC
|
||||||
|
handle_health_failure() {
|
||||||
|
local pool="$1"
|
||||||
|
local failure_reason="$2"
|
||||||
|
|
||||||
|
# Log box pour visibilité maximale
|
||||||
|
log "error" "╔════════════════════════════════════════════════════════════╗"
|
||||||
|
log "error" "║ ÉCHEC CRITIQUE DE SANTÉ POUR ${pool}"
|
||||||
|
log "error" "║ Raison: ${failure_reason}"
|
||||||
|
log "error" "╚════════════════════════════════════════════════════════════╝"
|
||||||
|
|
||||||
|
# Vérifier s'il y a eu une erreur récente (mécanisme anti-ping-pong)
|
||||||
|
if check_recent_critical_error "$pool"; then
|
||||||
|
local last_error_time
|
||||||
|
last_error_time=$(grep "^timestamp=" "${STATE_DIR}/critical-errors-${pool}.txt" | cut -d= -f2)
|
||||||
|
|
||||||
|
log "error" "Erreur critique récente détectée (${last_error_time})"
|
||||||
|
log "error" "Action: ARRÊT du LXC ${CTID} pour éviter ping-pong"
|
||||||
|
|
||||||
|
# Arrêter le LXC
|
||||||
|
if pct stop "$CTID" >/dev/null 2>&1; then
|
||||||
|
log "error" "✓ LXC ${CTID} arrêté avec succès"
|
||||||
|
send_notification "error" "Arrêt LXC (anti-ping-pong)" \
|
||||||
|
"Le LXC ${CTID} a été arrêté pour éviter un ping-pong.
|
||||||
|
|
||||||
|
Pool: ${pool}
|
||||||
|
Raison: ${failure_reason}
|
||||||
|
Erreur précédente: ${last_error_time}
|
||||||
|
|
||||||
|
ACTION REQUISE: Vérifier l'état des disques et pools sur les deux nœuds avant de redémarrer le LXC."
|
||||||
|
record_critical_error "$pool" "$failure_reason" "lxc_stopped"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "✗ Échec de l'arrêt du LXC ${CTID}"
|
||||||
|
record_critical_error "$pool" "$failure_reason" "lxc_stop_failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Première erreur ou erreur ancienne (> 1 heure)
|
||||||
|
log "warning" "Première erreur ou erreur ancienne (> ${HEALTH_CHECK_ERROR_COOLDOWN}s)"
|
||||||
|
log "warning" "Action: MIGRATION du LXC ${CTID} vers ${REMOTE_NODE_NAME}"
|
||||||
|
|
||||||
|
# Tenter la migration via HA
|
||||||
|
if ha-manager migrate "ct:${CTID}" "$REMOTE_NODE_NAME" >/dev/null 2>&1; then
|
||||||
|
log "warning" "✓ Migration HA initiée vers ${REMOTE_NODE_NAME}"
|
||||||
|
send_notification "error" "Migration LXC vers ${REMOTE_NODE_NAME}" \
|
||||||
|
"Le LXC ${CTID} a été migré vers ${REMOTE_NODE_NAME} suite à un problème de santé.
|
||||||
|
|
||||||
|
Pool: ${pool}
|
||||||
|
Raison: ${failure_reason}
|
||||||
|
Nœud source: $(hostname)
|
||||||
|
Nœud destination: ${REMOTE_NODE_NAME}
|
||||||
|
|
||||||
|
Le service NFS devrait continuer à fonctionner sur le nœud distant."
|
||||||
|
record_critical_error "$pool" "$failure_reason" "lxc_migrated"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "✗ Échec de la migration HA vers ${REMOTE_NODE_NAME}"
|
||||||
|
log "error" "Tentative d'arrêt du LXC en dernier recours"
|
||||||
|
|
||||||
|
# Fallback: arrêter le LXC si la migration échoue
|
||||||
|
if pct stop "$CTID" >/dev/null 2>&1; then
|
||||||
|
log "error" "✓ LXC ${CTID} arrêté en dernier recours"
|
||||||
|
record_critical_error "$pool" "$failure_reason" "lxc_stopped_failsafe"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log "error" "✗ Échec critique: impossible de migrer ou d'arrêter le LXC ${CTID}"
|
||||||
|
record_critical_error "$pool" "$failure_reason" "all_actions_failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Orchestrateur principal de vérification de santé d'un pool
|
||||||
|
verify_pool_health() {
|
||||||
|
local pool="$1"
|
||||||
|
|
||||||
|
export CURRENT_POOL="$pool"
|
||||||
|
log "info" "=========================================="
|
||||||
|
log "info" "Vérification de santé du pool: ${pool}"
|
||||||
|
log "info" "=========================================="
|
||||||
|
|
||||||
|
# Étape 1: Initialiser le tracking des disques si nécessaire
|
||||||
|
local uuids_file="${STATE_DIR}/disk-uuids-${pool}.txt"
|
||||||
|
if [[ ! -f "$uuids_file" ]] || ! grep -q "^initialized=true" "$uuids_file" 2>/dev/null; then
|
||||||
|
log "info" "Première exécution: initialisation du suivi des disques pour ${pool}"
|
||||||
|
|
||||||
|
if ! init_disk_tracking "$pool"; then
|
||||||
|
log "warning" "Impossible d'initialiser le suivi des disques pour ${pool}"
|
||||||
|
log "info" "Continuation sans vérification des disques (pool virtuel possible)"
|
||||||
|
# Ne pas échouer pour les pools virtuels
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Étape 2: Triple vérification de santé
|
||||||
|
if ! triple_health_check "$pool"; then
|
||||||
|
log "error" "Triple vérification de santé échouée pour ${pool}"
|
||||||
|
|
||||||
|
# Déterminer la raison de l'échec pour un message d'erreur précis
|
||||||
|
local failure_reason="Vérification de santé échouée"
|
||||||
|
|
||||||
|
# Tenter de déterminer la cause spécifique
|
||||||
|
if ! verify_disk_presence "$pool"; then
|
||||||
|
failure_reason="Disque(s) manquant(s) détecté(s)"
|
||||||
|
elif ! check_pool_health_status "$pool"; then
|
||||||
|
# Obtenir plus de détails sur le problème de pool
|
||||||
|
local pool_health
|
||||||
|
pool_health=$(zpool list -H -o health "$pool" 2>/dev/null)
|
||||||
|
if [[ "$pool_health" != "ONLINE" ]]; then
|
||||||
|
failure_reason="Pool en état ${pool_health}"
|
||||||
|
else
|
||||||
|
failure_reason="État du pool non nominal (espace disque ou erreurs I/O)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gérer l'échec (migration ou arrêt du LXC)
|
||||||
|
handle_health_failure "$pool" "$failure_reason"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "info" "✓ Vérification de santé réussie pour ${pool}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Fonction de réplication d'un pool
|
# Fonction de réplication d'un pool
|
||||||
replicate_pool() {
|
replicate_pool() {
|
||||||
local pool="$1"
|
local pool="$1"
|
||||||
|
|
@ -600,6 +1228,11 @@ replicate_pool() {
|
||||||
# SCRIPT PRINCIPAL
|
# SCRIPT PRINCIPAL
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
|
# Ne pas exécuter le script principal si on est en mode test BATS
|
||||||
|
if [[ "${BATS_TEST_MODE:-false}" == "true" ]]; then
|
||||||
|
return 0 2>/dev/null || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Initialiser le système de logs
|
# Initialiser le système de logs
|
||||||
init_logging
|
init_logging
|
||||||
|
|
||||||
|
|
@ -614,21 +1247,49 @@ log "info" "=========================================="
|
||||||
# Vérifier les mises à jour (avant toute opération)
|
# Vérifier les mises à jour (avant toute opération)
|
||||||
auto_update "$@"
|
auto_update "$@"
|
||||||
|
|
||||||
|
# Vérifier et initialiser la configuration des notifications
|
||||||
|
if [[ "${NOTIFICATION_ENABLED}" == "true" ]] && [[ -n "${APPRISE_URLS}" ]]; then
|
||||||
|
log "info" "Initialisation du système de notifications..."
|
||||||
|
|
||||||
|
# Initialiser l'environnement Python et installer Apprise si nécessaire
|
||||||
|
if setup_apprise_venv; then
|
||||||
|
local url_count
|
||||||
|
url_count=$(echo "${APPRISE_URLS}" | wc -w)
|
||||||
|
log "info" "✓ Notifications activées: ${url_count} service(s) configuré(s)"
|
||||||
|
log "info" " Mode: ${NOTIFICATION_MODE}"
|
||||||
|
else
|
||||||
|
log "warning" "Échec de l'initialisation d'Apprise - notifications désactivées"
|
||||||
|
NOTIFICATION_ENABLED=false
|
||||||
|
fi
|
||||||
|
elif [[ "${NOTIFICATION_ENABLED}" == "true" ]] && [[ -z "${APPRISE_URLS}" ]]; then
|
||||||
|
log "info" "Notifications activées mais aucune URL Apprise configurée (APPRISE_URLS vide)"
|
||||||
|
log "info" "Configurer APPRISE_URLS dans le script pour recevoir des notifications"
|
||||||
|
fi
|
||||||
|
|
||||||
# Déterminer le nœud distant et son IP
|
# Déterminer le nœud distant et son IP
|
||||||
case "$LOCAL_NODE" in
|
# Vérifier que le nœud local est dans la configuration
|
||||||
"acemagician")
|
if [[ ! -v "CLUSTER_NODES[$LOCAL_NODE]" ]]; then
|
||||||
REMOTE_NODE_NAME="elitedesk"
|
local valid_nodes="${!CLUSTER_NODES[@]}"
|
||||||
REMOTE_NODE_IP="192.168.100.20"
|
log "error" "Nœud inconnu: ${LOCAL_NODE}. Nœuds valides: ${valid_nodes}"
|
||||||
;;
|
exit 1
|
||||||
"elitedesk")
|
fi
|
||||||
REMOTE_NODE_NAME="acemagician"
|
|
||||||
REMOTE_NODE_IP="192.168.100.10"
|
# Trouver le nœud distant (le premier nœud différent du local)
|
||||||
;;
|
REMOTE_NODE_NAME=""
|
||||||
*)
|
REMOTE_NODE_IP=""
|
||||||
log "error" "Nœud inconnu: ${LOCAL_NODE}. Ce script doit s'exécuter sur acemagician ou elitedesk."
|
for node in "${!CLUSTER_NODES[@]}"; do
|
||||||
exit 1
|
if [[ "$node" != "$LOCAL_NODE" ]]; then
|
||||||
;;
|
REMOTE_NODE_NAME="$node"
|
||||||
esac
|
REMOTE_NODE_IP="${CLUSTER_NODES[$node]}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier qu'un nœud distant a été trouvé
|
||||||
|
if [[ -z "$REMOTE_NODE_NAME" ]]; then
|
||||||
|
log "error" "Aucun nœud distant trouvé dans CLUSTER_NODES. Vérifier la configuration."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
log "info" "Nœud distant configuré: ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP})"
|
log "info" "Nœud distant configuré: ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP})"
|
||||||
log "info" "Pools configurés: ${ZPOOLS[*]}"
|
log "info" "Pools configurés: ${ZPOOLS[*]}"
|
||||||
|
|
@ -652,6 +1313,29 @@ fi
|
||||||
|
|
||||||
log "info" "Connexion SSH vers ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP}) vérifiée"
|
log "info" "Connexion SSH vers ${REMOTE_NODE_NAME} (${REMOTE_NODE_IP}) vérifiée"
|
||||||
|
|
||||||
|
# Vérification de santé des pools
|
||||||
|
log "info" "=========================================="
|
||||||
|
log "info" "Vérification de santé des pools"
|
||||||
|
log "info" "=========================================="
|
||||||
|
|
||||||
|
HEALTH_CHECK_FAILED=false
|
||||||
|
for pool in "${ZPOOLS[@]}"; do
|
||||||
|
if ! verify_pool_health "$pool"; then
|
||||||
|
log "error" "Vérification de santé échouée pour ${pool}"
|
||||||
|
HEALTH_CHECK_FAILED=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Arrêt si échec de santé détecté
|
||||||
|
if [[ "$HEALTH_CHECK_FAILED" == "true" ]]; then
|
||||||
|
export CURRENT_POOL="global"
|
||||||
|
log "error" "Arrêt du script suite à échec(s) de vérification de santé"
|
||||||
|
log "error" "Le LXC a été migré ou arrêté pour protection"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "info" "✓ Tous les pools sont en bonne santé"
|
||||||
|
|
||||||
# Compteurs globaux
|
# Compteurs globaux
|
||||||
POOLS_TOTAL=${#ZPOOLS[@]}
|
POOLS_TOTAL=${#ZPOOLS[@]}
|
||||||
POOLS_SUCCESS=0
|
POOLS_SUCCESS=0
|
||||||
|
|
@ -683,8 +1367,23 @@ log "info" "=========================================="
|
||||||
|
|
||||||
if [[ $POOLS_FAILED -eq 0 ]]; then
|
if [[ $POOLS_FAILED -eq 0 ]]; then
|
||||||
log "info" "✓ Toutes les réplications ont réussi"
|
log "info" "✓ Toutes les réplications ont réussi"
|
||||||
|
send_notification "info" "Réplication ZFS réussie" \
|
||||||
|
"Toutes les réplications ZFS ont réussi.
|
||||||
|
|
||||||
|
Pools répliqués: ${POOLS_TOTAL}
|
||||||
|
Succès: ${POOLS_SUCCESS}
|
||||||
|
Nœud actif: $(hostname)
|
||||||
|
Nœud distant: ${REMOTE_NODE_NAME}"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
log "error" "✗ ${POOLS_FAILED} pool(s) ont échoué"
|
log "error" "✗ ${POOLS_FAILED} pool(s) ont échoué"
|
||||||
|
send_notification "error" "Échec réplication ZFS" \
|
||||||
|
"${POOLS_FAILED} pool(s) ont échoué lors de la réplication.
|
||||||
|
|
||||||
|
Total: ${POOLS_TOTAL}
|
||||||
|
Succès: ${POOLS_SUCCESS}
|
||||||
|
Échecs: ${POOLS_FAILED}
|
||||||
|
|
||||||
|
Vérifier les logs: /var/log/zfs-nfs-replica/"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue