first commit
This commit is contained in:
171
README.md
Normal file
171
README.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# 🔋 Protection Proxmox via Onduleur EATON Ellipse PRO avec NUT
|
||||||
|
|
||||||
|
## ✅ Objectif
|
||||||
|
|
||||||
|
Assurer un **arrêt automatique propre** de toutes les VMs puis du serveur **Proxmox VE** en cas de coupure de courant, en s'appuyant sur l'onduleur **EATON Ellipse PRO USB** et le logiciel **NUT (Network UPS Tools)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Composants
|
||||||
|
|
||||||
|
* **UPS** : EATON Ellipse PRO 850 VA (USB)
|
||||||
|
* **Hôte Proxmox VE** (relié physiquement via USB)
|
||||||
|
* **Logiciel** : `nut`, `nut-client`, `jq`
|
||||||
|
* **Notifications** : Webhook Discord
|
||||||
|
* **Scripts personnalisés** : `/etc/nut/upssched-cmd.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Fonctionnement
|
||||||
|
|
||||||
|
| Événement | Action |
|
||||||
|
| ------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| Passage sur batterie (`onbatt`) | Envoie un message Discord avec état complet de l'UPS |
|
||||||
|
| Retour secteur (`online`) | Message Discord "Retour secteur" |
|
||||||
|
| Batterie faible (`shutdown`) | Arrêt propre des VMs (via `qm shutdown`) puis du serveur PVE |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Installation & configuration
|
||||||
|
|
||||||
|
### 1. Installer les paquets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt update -y && apt install -y nut jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Détection de l'UPS (via USB)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nut-scanner -U
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirmer que le modèle EATON est bien détecté.
|
||||||
|
|
||||||
|
### 3. Fichiers de configuration NUT
|
||||||
|
|
||||||
|
#### `/etc/nut/ups.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[eaton]
|
||||||
|
driver = usbhid-ups
|
||||||
|
port = auto
|
||||||
|
vendorid = 0463
|
||||||
|
productid = FFFF
|
||||||
|
desc = "Onduleur EATON Ellipse PRO"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `/etc/nut/nut.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
MODE=standalone
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `/etc/nut/upsd.users`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[monuser]
|
||||||
|
password = secret
|
||||||
|
upsmon master
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `/etc/nut/upsmon.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
MONITOR eaton@localhost 1 monuser secret master
|
||||||
|
NOTIFYCMD /sbin/upssched
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `/etc/nut/upssched.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
CMDSCRIPT /etc/nut/upssched-cmd.sh
|
||||||
|
PIPEFN /var/run/nut/upssched.pipe
|
||||||
|
LOCKFN /var/run/nut/upssched.lock
|
||||||
|
|
||||||
|
AT COMMBAD * EXECUTE onbatt
|
||||||
|
AT COMMOK * EXECUTE online
|
||||||
|
AT LOWBATT * EXECUTE shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔢 Script `/etc/nut/upssched-cmd.sh`
|
||||||
|
|
||||||
|
### Fonctions incluses :
|
||||||
|
|
||||||
|
* Lecture de l'état de l'UPS via `upsc`
|
||||||
|
* Log local horodaté `/var/log/ups-shutdown.log`
|
||||||
|
* Notification Webhook Discord (format Markdown)
|
||||||
|
* Arrêt propre de toutes les VMs via `qm shutdown`
|
||||||
|
* Attente de l'extinction totale avant `shutdown -h now`
|
||||||
|
|
||||||
|
### Sécurité :
|
||||||
|
|
||||||
|
Le script contient une variable `SIMULATION=false` pour activer le mode production.
|
||||||
|
Si mise à `true`, aucun arrêt ne sera réellement effectué (mode test).
|
||||||
|
|
||||||
|
### Autorisations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /etc/nut/upssched-cmd.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Notifications Discord
|
||||||
|
|
||||||
|
Chaque événement envoie un message via Webhook (texte ou bloc `markdown`).
|
||||||
|
|
||||||
|
Exemple `onbatt` :
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
🔕 pve est passé sur batterie à 2025-07-29 09:10:22
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
🖥️ Modèle : Ellipse PRO 850
|
||||||
|
🔋 Charge batterie : 46 %
|
||||||
|
⏳ Autonomie estimée : 168 sec
|
||||||
|
⚡ Charge appliquée : 47 %
|
||||||
|
🔌 Entrée : 237.0 V → ⚡ Sortie : 234.0 V
|
||||||
|
🔋 Puissance : 261 VA
|
||||||
|
💡 Statut UPS : OL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Test manuel sans danger
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/etc/nut/upssched-cmd.sh onbatt
|
||||||
|
```
|
||||||
|
|
||||||
|
Permet de vérifier les logs + Discord sans exécuter d'arrêt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔺 Bascule en production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /etc/nut/upssched-cmd.sh
|
||||||
|
# Remplacer : SIMULATION=true --> SIMULATION=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis redémarrer NUT :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl restart nut-server
|
||||||
|
systemctl restart nut-client
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Log
|
||||||
|
|
||||||
|
Fichier log local : `/var/log/ups-shutdown.log`
|
||||||
|
Contient tous les événements UPS avec horodatage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💼 Auteur : Ssyleric — 2025-07-29
|
||||||
17
check-ups-runtime.sh
Normal file
17
check-ups-runtime.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
WEBHOOK="https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
LOGFILE="/var/log/ups-shutdown.log"
|
||||||
|
|
||||||
|
UPS_DATA=$(upsc eaton@localhost 2>/dev/null)
|
||||||
|
STATUS=$(echo "$UPS_DATA" | grep '^ups.status:' | awk '{print $2}')
|
||||||
|
RUNTIME=$(echo "$UPS_DATA" | grep '^battery.runtime:' | awk '{print $2}')
|
||||||
|
BATTERY_CHARGE=$(echo "$UPS_DATA" | grep '^battery.charge:' | awk '{print $2}')
|
||||||
|
MODEL=$(echo "$UPS_DATA" | grep '^device.model:' | cut -d ':' -f2- | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [[ "$STATUS" == "OB" && "$RUNTIME" -lt 300 ]]; then
|
||||||
|
MESSAGE="⏱ *$(hostname)* — autonomie critique détectée !\n🔋 Batterie : ${BATTERY_CHARGE}%\n⏳ Autonomie : ${RUNTIME} sec\n🖥️ Modèle : $MODEL"
|
||||||
|
echo "$(date '+%F %T') ⚠️ Recheck runtime < 300 sec (batt=$BATTERY_CHARGE%, runtime=$RUNTIME sec)" >> "$LOGFILE"
|
||||||
|
jq -n --arg content "$MESSAGE" '{content: $content}' | \
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d @- "$WEBHOOK" > /dev/null
|
||||||
|
fi
|
||||||
27
check-ups.sh
Normal file
27
check-ups.sh
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
WEBHOOK="https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
UPS_DATA=$(upsc eaton@localhost 2>/dev/null)
|
||||||
|
|
||||||
|
BATTERY_CHARGE=$(echo "$UPS_DATA" | grep '^battery.charge:' | awk '{print $2}')
|
||||||
|
RUNTIME=$(echo "$UPS_DATA" | grep '^battery.runtime:' | awk '{print $2}')
|
||||||
|
LOAD=$(echo "$UPS_DATA" | grep '^ups.load:' | awk '{print $2}')
|
||||||
|
STATUS=$(echo "$UPS_DATA" | grep '^ups.status:' | awk '{print $2}')
|
||||||
|
INPUT_VOLT=$(echo "$UPS_DATA" | grep '^input.voltage:' | awk '{print $2}')
|
||||||
|
OUTPUT_VOLT=$(echo "$UPS_DATA" | grep '^output.voltage:' | awk '{print $2}')
|
||||||
|
POWER=$(echo "$UPS_DATA" | grep '^ups.power:' | awk '{print $2}')
|
||||||
|
MODEL=$(echo "$UPS_DATA" | grep '^device.model:' | cut -d ':' -f2- | sed 's/^ *//')
|
||||||
|
|
||||||
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
MESSAGE="🕓 *État UPS à ${TIMESTAMP}*
|
||||||
|
🖥️ Modèle : $MODEL
|
||||||
|
🔋 Charge batterie : ${BATTERY_CHARGE} %
|
||||||
|
⏳ Autonomie estimée : ${RUNTIME} sec
|
||||||
|
⚡ Charge appliquée : ${LOAD} %
|
||||||
|
🔌 Entrée : ${INPUT_VOLT} V → ⚡ Sortie : ${OUTPUT_VOLT} V
|
||||||
|
🔋 Puissance : ${POWER} VA
|
||||||
|
💡 Statut UPS : $STATUS"
|
||||||
|
|
||||||
|
jq -n --arg content "$MESSAGE" '{content: $content}' | \
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d @- "$WEBHOOK" > /dev/null
|
||||||
107
upssched-cmd.sh
Normal file
107
upssched-cmd.sh
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
LOGFILE="/var/log/ups-shutdown.log"
|
||||||
|
WEBHOOK="https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
SIMULATION=false # ✅ mode production (⚠️ coupe vraiment les VMs et le serveur)
|
||||||
|
|
||||||
|
send_discord() {
|
||||||
|
MESSAGE="$1"
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d "{\"content\": \"$MESSAGE\"}" "$WEBHOOK" > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
send_log_discord() {
|
||||||
|
LOG_CONTENT=$(tail -n 50 "$LOGFILE" | head -c 1900 | jq -Rs .)
|
||||||
|
jq -n --arg text "🔻 \`$(hostname)\` extinction imminente — $(date '+%F %T')\n\n" \
|
||||||
|
--arg log "$LOG_CONTENT" \
|
||||||
|
'{content: ($text + "```" + $log + "```")}' |
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d @- "$WEBHOOK" > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
send_discord_onbatt() {
|
||||||
|
UPS_DATA=$(upsc eaton@localhost 2>/dev/null)
|
||||||
|
|
||||||
|
BATTERY_CHARGE=$(echo "$UPS_DATA" | grep '^battery.charge:' | awk '{print $2}')
|
||||||
|
RUNTIME=$(echo "$UPS_DATA" | grep '^battery.runtime:' | awk '{print $2}')
|
||||||
|
LOAD=$(echo "$UPS_DATA" | grep '^ups.load:' | awk '{print $2}')
|
||||||
|
STATUS=$(echo "$UPS_DATA" | grep '^ups.status:' | awk '{print $2}')
|
||||||
|
INPUT_VOLT=$(echo "$UPS_DATA" | grep '^input.voltage:' | awk '{print $2}')
|
||||||
|
OUTPUT_VOLT=$(echo "$UPS_DATA" | grep '^output.voltage:' | awk '{print $2}')
|
||||||
|
POWER=$(echo "$UPS_DATA" | grep '^ups.power:' | awk '{print $2}')
|
||||||
|
MODEL=$(echo "$UPS_DATA" | grep '^device.model:' | cut -d ':' -f2- | sed 's/^ *//')
|
||||||
|
|
||||||
|
LOG_FORMAT=$(cat <<EOF
|
||||||
|
🖥️ Modèle : $MODEL
|
||||||
|
🔋 Charge batterie : ${BATTERY_CHARGE} %
|
||||||
|
⏳ Autonomie estimée : ${RUNTIME} sec
|
||||||
|
⚡ Charge appliquée : ${LOAD} %
|
||||||
|
🔌 Entrée : ${INPUT_VOLT} V → ⚡ Sortie : ${OUTPUT_VOLT} V
|
||||||
|
🔋 Puissance : ${POWER} VA
|
||||||
|
💡 Statut UPS : $STATUS
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg msg "⚠️ *$(hostname)* est passé sur **batterie** à **$(date '+%Y-%m-%d %H:%M:%S')** 🔋" \
|
||||||
|
--arg log "$LOG_FORMAT" \
|
||||||
|
'{content: ($msg + "\n```" + $log + "```")}'
|
||||||
|
)
|
||||||
|
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "$WEBHOOK" > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
check_runtime_warning() {
|
||||||
|
UPS_DATA=$(upsc eaton@localhost 2>/dev/null)
|
||||||
|
RUNTIME=$(echo "$UPS_DATA" | grep '^battery.runtime:' | awk '{print $2}')
|
||||||
|
BATTERY_CHARGE=$(echo "$UPS_DATA" | grep '^battery.charge:' | awk '{print $2}')
|
||||||
|
MODEL=$(echo "$UPS_DATA" | grep '^device.model:' | cut -d ':' -f2- | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [[ "$RUNTIME" -lt 300 ]]; then
|
||||||
|
MESSAGE="⚠️ *$(hostname)* — autonomie UPS **trop faible** malgré batterie à ${BATTERY_CHARGE}%\nModèle: $MODEL\n⏳ Autonomie estimée : ${RUNTIME} sec"
|
||||||
|
jq -n --arg content "$MESSAGE" '{content: $content}' |
|
||||||
|
curl -s -H "Content-Type: application/json" -X POST -d @- "$WEBHOOK" > /dev/null
|
||||||
|
echo "$(date '+%F %T') 🚨 Alerte : autonomie faible avec charge ${BATTERY_CHARGE}% (runtime=${RUNTIME})" >> "$LOGFILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
onbatt)
|
||||||
|
logger "[NUT] ⚠️ Passage sur batterie détecté"
|
||||||
|
echo "$(date '+%F %T') ⚠️ UPS on battery" >> "$LOGFILE"
|
||||||
|
send_discord_onbatt
|
||||||
|
check_runtime_warning
|
||||||
|
;;
|
||||||
|
|
||||||
|
online)
|
||||||
|
logger "[NUT] 🔌 Retour à l'alimentation secteur"
|
||||||
|
echo "$(date '+%F %T') 🔌 Retour secteur" >> "$LOGFILE"
|
||||||
|
send_discord "🔌 **$(hostname)** est revenu sur **secteur** à **$(date '+%Y-%m-%d %H:%M:%S')** ⚡"
|
||||||
|
;;
|
||||||
|
|
||||||
|
shutdown)
|
||||||
|
logger "[NUT] 🛑 Batterie faible — extinction imminente (simulation=$SIMULATION)"
|
||||||
|
echo "$(date '+%F %T') 🛑 Batterie faible — arrêt des VMs + Proxmox (simulation=$SIMULATION)" >> "$LOGFILE"
|
||||||
|
send_discord "🛑 **$(hostname)** — batterie faible détectée à **$(date '+%Y-%m-%d %H:%M:%S')** ⚡\nArrêt des VMs en cours (simulation=$SIMULATION)..."
|
||||||
|
|
||||||
|
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
|
||||||
|
echo "$(date '+%F %T') → Arrêt VM $vmid" >> "$LOGFILE"
|
||||||
|
if [[ "$SIMULATION" != "true" ]]; then
|
||||||
|
qm shutdown $vmid --timeout 300
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$(date '+%F %T') ⏳ Attente extinction des VMs…" >> "$LOGFILE"
|
||||||
|
if [[ "$SIMULATION" != "true" ]]; then
|
||||||
|
while qm list | awk 'NR>1 {print $2}' | grep -q running; do
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date '+%F %T') ✅ VMs arrêtées" >> "$LOGFILE"
|
||||||
|
echo "$(date '+%F %T') 🔻 Shutdown final (simulation=$SIMULATION)" >> "$LOGFILE"
|
||||||
|
send_log_discord
|
||||||
|
|
||||||
|
if [[ "$SIMULATION" != "true" ]]; then
|
||||||
|
shutdown -h now
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user