Post

đŸ–„ CWWK - Debian 13 - Alertes services systemd en erreur via ntfy

đŸ–„ CWWK - Debian 13 - Alertes services systemd en erreur via ntfy

debian linux, comment ĂȘtre alertĂ© lorsqu’un service systemd est en Ă©chec

Alerte service défaillant

Script envoi (/usr/local/bin/notify-ntfy.sh)

/usr/local/bin/notify-ntfy.sh (exĂ©cutable) — envoie la notification avec token, collecte journalctl (200 lignes), throttle 60s par unitĂ©, supprime fichier temporaire.

Voici la version mise Ă  jour et plus robuste de /usr/local/bin/notify-ntfy.sh. Elle :

  • dĂ©tecte l’unitĂ© dĂ©clenchante via plusieurs heuristiques (extraction de journal, systemctl –failed),
  • nettoie le nom d’unitĂ© pour Ă©viter caractĂšres non imprimables,
  • Ă©vite les auto‑dĂ©clenchements (notify-ntfy.service),
  • conserve throttling et en‑tĂȘte Email/token ntfy.

Remplacez l’ancien script par celui‑ci, chmod 755 /usr/local/bin/notify-ntfy.sh, puis testez. Fichier /usr/local/bin/notify-ntfy.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/bin/sh
# notify-ntfy.sh
# Usage: notify-ntfy.sh <unit> <instance> <exitstatus>
# Envoie une notification Ă  ntfy.rnmkcy.eu/yan_infos avec token et header Email.
# Robustification : heuristiques pour détecter l'unité déclenchante et nettoyage du nom.

NTFY_URL="https://ntfy.rnmkcy.eu/yan_infos"
TOKEN="tk_9h2bfxjs0pkbuwsnavc6w0wua5t5x"
EMAIL_HEADER="ntfy@cinay.eu"
HOST="$(hostname -f)"

# Params fournis par systemd
UNIT_PARAM="$1"
INSTANCE="$2"
EXITSTATUS="$3"

# Nettoyage simple d'une chaĂźne (supprime caractĂšres non imprimables)
clean() {
  printf '%s' "$1" | tr -cd '[:print:]' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}

# Détecter l'unité la plus récemment en erreur dans le journal
detect_unit_from_journal() {
  # Cherche d'abord des occurrences explicites d'unités dans messages d'erreur récents
  journalctl -b -n 1000 --no-pager -p err,crit,alert,emerg 2>/dev/null \
    | tac \
    | sed -n '1,500p' \
    | grep -m1 -Eo '[A-Za-z0-9_.@-]+\.service' \
    || true
}

# Détecter unité via messages systemd (Unit=... or Failed)
detect_unit_from_journal_alt() {
  journalctl -b -n 1000 --no-pager -o short-precise 2>/dev/null \
    | tac \
    | sed -n '1,500p' \
    | grep -m1 -Eo 'Unit=[A-Za-z0-9_.@-]+\.service' \
    | sed 's/^Unit=//g' \
    || true
}

# Détecter via systemctl --failed
detect_unit_from_systemctl_failed() {
  systemctl --failed --no-legend --no-pager 2>/dev/null | awk '{print $1}' | head -n1 || true
}

# Resolve UNIT
UNIT="$(clean "$UNIT_PARAM")"

# If parameter absent or looks wrong, try heuristics
if [ -z "$UNIT" ] || [ "$UNIT" = "notify-ntfy.service" ] || [ -z "$EXITSTATUS" ] || printf '%s' "$EXITSTATUS" | grep -qE '/bin/(sh|bash|dash)'; then
  DETECTED="$(detect_unit_from_journal)"
  if [ -z "$DETECTED" ]; then
    DETECTED="$(detect_unit_from_journal_alt)"
  fi
  if [ -z "$DETECTED" ]; then
    DETECTED="$(detect_unit_from_systemctl_failed)"
  fi
  DETECTED="$(clean "$DETECTED")"
  if [ -n "$DETECTED" ] && [ "$DETECTED" != "notify-ntfy.service" ]; then
    UNIT="$DETECTED"
  fi
fi

# Final fallback
[ -z "$UNIT" ] && UNIT="unknown"

# Prepare tmp log
TMPLOG="$(mktemp /tmp/journal-${UNIT}.XXXXXX 2>/dev/null || echo /tmp/journal-${UNIT}.$$)"

# Collect unit journal (if unit known)
journalctl -u "$UNIT" -n 200 --no-pager > "$TMPLOG" 2>/dev/null || true

TIMESTAMP="$(date --iso-8601=seconds)"
UNIT_CLEAN="$(printf '%s' "$UNIT" | tr -cd '[:alnum:].@_-')"
[ -z "$UNIT_CLEAN" ] && UNIT_CLEAN="unknown"
TITLE="${UNIT_CLEAN} failed on ${HOST}"
BODY="${TIMESTAMP} - ${HOST} - ${UNIT}
detected_unit: ${UNIT}
instance: ${INSTANCE}
exitstatus: ${EXITSTATUS}

Last journal lines:
$(tail -n 60 "$TMPLOG" 2>/dev/null)
"

# Throttle: one alert per unit per 60s
STATE_DIR="/var/lib/notify-ntfy"
mkdir -p "$STATE_DIR"
LASTFILE="$STATE_DIR/last-${UNIT_CLEAN}"
NOW=$(date +%s)
if [ -f "$LASTFILE" ]; then
  LAST=$(cat "$LASTFILE" 2>/dev/null || echo 0)
else
  LAST=0
fi
THROTTLE=60
if [ $((NOW - LAST)) -lt $THROTTLE ]; then
  rm -f "$TMPLOG"
  exit 0
fi
printf '%s' "$NOW" > "$LASTFILE"

echo "$TITLE script notify-ntfy.sh" | systemd-cat -t notify -p info

# -H "Email: $EMAIL_HEADER" \
# -H "Content-Type: text/plain; charset=utf-8" \

RESUL=`journalctl -u notify-ntfy@$UNIT_PARAM -n 10 --no-pager`

# Send to ntfy
curl -sS -X POST "$NTFY_URL" \
 -H "Title: $TITLE" \
 -H "Priority: high" \
 -H "Authorization: Bearer $TOKEN" \
 -d "$RESUL"

rm -f "$TMPLOG"
exit 0

Rendre exécutable

1
chmod 755 /usr/local/bin/notify-ntfy.sh

Unité systemd (/etc/systemd/system/notify-ntfy@.service)

Voici la version template prĂȘte Ă  installer — crĂ©ez /etc/systemd/system/notify-ntfy@.service avec ce contenu, puis sudo systemctl daemon-reload.

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Notify ntfy for failed unit %i
# Ne pas lier au service fautif pour éviter cycles
RefuseManualStart=no

[Service]
Type=oneshot
# %i = instance (nom de l'unité fournie par OnFailure=notify-ntfy@%n.service)
ExecStart=/usr/local/bin/notify-ntfy.sh %i "" "1"
Restart=no

Services Ă  inclure dans les alertes (/root/gen-and-confirm-authorize.sh)

Voici la version renommĂ©e et adaptĂ©e en /root/gen-and-confirm-authorize.sh — interactive par dĂ©faut, avec option --all pour ajouter toutes les unitĂ©s dans /root/AUTHORIZED_LIST. ExĂ©cution en root.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/sh
# gen-and-confirm-authorize.sh
# Usage:
#   sudo /root/gen-and-confirm-authorize.sh        # interactif
#   sudo /root/gen-and-confirm-authorize.sh --all  # ajoute tous les services Ă  AUTHORIZED_LIST
#
SERVICES_OUT="/root/services-list.txt"
AUTHORIZED_FILE="/root/AUTHORIZED_LIST"

set -eu

# Générer la liste complÚte des services
systemctl list-units --type=service --all --no-legend --no-pager | awk '{print $1}' > "$SERVICES_OUT"
echo "Wrote $(wc -l < "$SERVICES_OUT") services to $SERVICES_OUT"

# Créer AUTHORIZED_FILE s'il n'existe pas
touch "$AUTHORIZED_FILE"

if [ "${1-}" = "--all" ]; then
  # Ajouter tous les services (unique, trié)
  cat "$SERVICES_OUT" "$AUTHORIZED_FILE" | sort -u > "${AUTHORIZED_FILE}.tmp"
  mv "${AUTHORIZED_FILE}.tmp" "$AUTHORIZED_FILE"
  echo "Added all services to $AUTHORIZED_FILE"
  exit 0
fi

echo "Pour chaque service, tapez y pour l'ajouter dans $AUTHORIZED_FILE, n pour ignorer, q pour quitter."

while IFS= read -r svc; do
  [ -z "$svc" ] && continue

  # skip if already present
  if grep -qxF "$svc" "$AUTHORIZED_FILE" 2>/dev/null; then
    continue
  fi

  printf 'Ajouter "%s" Ă  %s ? [y/N/q]: ' "$svc" "$AUTHORIZED_FILE"
  read -r ans || ans="n"
  case "$ans" in
    y|Y)
      echo "$svc" >> "$AUTHORIZED_FILE"
      echo "Ajouté."
      ;;
    q|Q)
      echo "Abandon."
      break
      ;;
    *)
      ;;
  esac
done < "$SERVICES_OUT"

echo "Fini. Editez $AUTHORIZED_FILE si nécessaire."
exit 0

Instructions :

  • Copier en /root/gen-and-confirm-authorize.sh
  • chmod 700 /root/gen-and-confirm-authorize.sh
  • ExĂ©cuter interactif : sudo /root/gen-and-confirm-authorize.sh
  • Ou ajouter tous en une fois : sudo /root/gen-and-confirm-authorize.sh --all

Ensuite éditez /root/EXCLUDE_LIST si besoin et lancez apply-onfailure.sh

Drop-in (/root/apply-onfailure.sh)

version mise Ă  jour du script /root/apply-onfailure.sh qui ajoute une option --force pour réécrire les drop‑ins existants (par dĂ©faut ils sont laissĂ©s intacts) et conserve --dry-run et --help puis chmod +x.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/bin/sh
# apply-onfailure.sh
# Usage:
#   sudo /root/apply-onfailure.sh             # applique les drop-ins pour les services listés dans AUTHORIZED_LIST
#   sudo /root/apply-onfailure.sh --dry-run   # n'écrit rien, affiche ce qui serait fait
#   sudo /root/apply-onfailure.sh --force     # réécrit les drop-ins existants
#   sudo /root/apply-onfailure.sh --help      # affiche cette aide
AUTHORIZED_FILE="/root/AUTHORIZED_LIST"
DRY_RUN=0
FORCE=0
set -eu

usage() {
  cat <<EOF
Usage: $0 [--dry-run] [--force] [--help]
  --dry-run   : show what would be done, do not write files
  --force     : overwrite existing drop-ins
  --help      : show this help
EOF
}

for arg in "$@"; do
  case "$arg" in
    --dry-run) DRY_RUN=1 ;;
    --force) FORCE=1 ;;
    --help) usage; exit 0 ;;
    *) printf 'Unknown argument: %s\n' "$arg" >&2; usage >&2; exit 2 ;;
  esac
done

if [ ! -f "$AUTHORIZED_FILE" ]; then
  printf 'Fichier %s introuvable. Créez-le avec une unité .service par ligne.\n' "$AUTHORIZED_FILE" >&2
  exit 1
fi

# Read authorized list
AUTHORIZED=""
while IFS= read -r line || [ -n "$line" ]; do
  line="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
  [ -z "$line" ] && continue
  case "$line" in \#*) continue ;; esac
  AUTHORIZED="$AUTHORIZED $line"
done < "$AUTHORIZED_FILE"

is_authorized() {
  svc="$1"
  for a in $AUTHORIZED; do
    [ "$svc" = "$a" ] && return 0
  done
  return 1
}

TO_CREATE=0
TO_OVERWRITE=0
printf 'Scanning services...\n'

systemctl list-units --type=service --all --no-legend --no-pager | awk '{print $1}' | while IFS= read -r u; do
  [ -z "$u" ] && continue
  if ! is_authorized "$u"; then
    printf 'SKIP (not authorized): %s\n' "$u"
    continue
  fi

  d="/etc/systemd/system/${u}.d"
  target="$d/10-onfailure.conf"

  if [ "$DRY_RUN" -eq 1 ]; then
    if [ -f "$target" ]; then
      if [ "$FORCE" -eq 1 ]; then
        printf 'WILL OVERWRITE: %s\n' "$target"
        TO_OVERWRITE=$((TO_OVERWRITE+1))
      else
        printf 'EXISTS: %s (will not overwrite)\n' "$target"
      fi
    else
      printf 'WILL CREATE: %s\n' "$target"
      TO_CREATE=$((TO_CREATE+1))
    fi
  else
    mkdir -p "$d"
    if [ -f "$target" ]; then
      if [ "$FORCE" -eq 1 ]; then
        cat > "$target" <<'EOF'
[Unit]
OnFailure=notify-ntfy@%n.service
EOF
        printf 'OVERWRITTEN: %s\n' "$target"
        TO_OVERWRITE=$((TO_OVERWRITE+1))
      else
        printf 'SKIP (already exists): %s\n' "$target"
      fi
    else
      cat > "$target" <<'EOF'
[Unit]
OnFailure=notify-ntfy@%n.service
EOF
      printf 'CREATED: %s\n' "$target"
      TO_CREATE=$((TO_CREATE+1))
    fi
  fi
done

if [ "$DRY_RUN" -eq 1 ]; then
  printf 'Dry run complete. %d drop-ins would be created, %d overwritten.\n' "$TO_CREATE" "$TO_OVERWRITE"
  exit 0
else
  systemctl daemon-reload
  printf 'Done. %d drop-ins created, %d overwritten. systemctl daemon-reload executed.\n' "$TO_CREATE" "$TO_OVERWRITE"
  exit 0
fi

Instructions :

  • Sauvegardez, rendez exĂ©cutable : chmod 700 /root/apply-onfailure.sh
  • Assurez-vous que /root/EXCLUDE_LIST existe (une unitĂ© .service par ligne, # pour commentaires).
  • ExĂ©cutez en root : sudo /root/apply-onfailure.sh

Lancement service

S’assurer que le service notify existe et le script est exĂ©cutable, puis reload

1
2
3
#chmod 755 /usr/local/bin/notify-ntfy.sh
sudo systemctl daemon-reload
sudo systemctl restart notify-ntfy

Tests

Tests rapides pour vérifier notify-ntfy@.service et le mécanisme OnFailure.

1) VĂ©rifier l’unitĂ© template et le script
Afficher le contenu de l’unitĂ© template :

1
sudo systemctl cat notify-ntfy@.service

Vérifier que le script existe et est exécutable :

1
2
sudo stat -c '%a %U:%G %n' /usr/local/bin/notify-ntfy.sh
sudo head -n 50 /usr/local/bin/notify-ntfy.sh

2) Tester l’unitĂ© template directement (dry-run d’exĂ©cution systemd)
Exécution simulée sans implicite dépendances :

1
sudo systemd-run --unit=notify-test --on-active=0 --description="test notify" --property=RemainAfterExit=no /usr/local/bin/notify-ntfy.sh test.service "" "1"

Puis :

1
sudo journalctl -u systemd-run\@notify-test.service -n 200

3) Lancer l’instance template directement
DĂ©marrer l’instance pour voir qu’elle s’exĂ©cute :

1
2
3
sudo systemctl start notify-ntfy@test.service
sudo systemctl status notify-ntfy@test.service
sudo journalctl -u notify-ntfy@test.service -n 200

Remarques : %i dans l’unitĂ© devient test.service.

4) Simuler un OnFailure réel depuis une unité de test
Créez une unité éphémÚre qui échoue (test-fail.service) :

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo tee /etc/systemd/system/test-fail.service > /dev/null <<'EOF'
[Unit]
Description=Test fail unit
OnFailure=notify-ntfy@%n.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo FAIL >&2; exit 1'
EOF
sudo systemctl daemon-reload
sudo systemctl start test-fail.service
sudo systemctl status test-fail.service
sudo journalctl -u notify-ntfy@test-fail.service -n 200

VĂ©rifiez que notify-ntfy@test-fail.service s’est lancĂ© et que votre script a Ă©tĂ© appelĂ©.

5) Tester intĂ©gration sur un service existant (sans modifier l’unitĂ© principale) Si apply-onfailure a créé le drop-in OnFailure=notify-ntfy@%n.service, forcez l’échec de la cible :

1
2
3
4
sudo systemctl kill -s SIGABRT gonic.service
sleep 1
sudo systemctl status gonic.service
sudo journalctl -u notify-ntfy@gonic.service -n 200

Si gonic est configuré Restart=on-failure et ne devient pas failed, utilisez le wrapper ou simulez avec une unité de test.

6) DĂ©pannage si notify ne s’exĂ©cute pas
Vérifiez journaux unitaires :

1
2
sudo journalctl -u notify-ntfy@<unit>.service -n 200
sudo journalctl -u <unit> -n 200

Vérifiez code retour et permissions du script :

1
sudo -u <user> /usr/local/bin/notify-ntfy.sh <unit> "" "1"

Vérifiez dépendances circulaires :

1
systemctl show notify-ntfy@<unit>.service --property=Wants,Requires,BindsTo

7) Nettoyage test

1
2
3
4
sudo systemctl stop notify-ntfy@test-fail.service || true
sudo systemctl disable --now test-fail.service || true
sudo rm -f /etc/systemd/system/test-fail.service
sudo systemctl daemon-reload
Cet article est sous licence CC BY 4.0 par l'auteur.