收起左侧

UPS状态切换邮件通知功能分享

0
回复
192
查看
[ 复制链接 ]

1

主题

8

回帖

0

牛值

江湖小虾

2025-6-30 10:20:17 显示全部楼层 阅读模式

适用于:飞牛OS (fnOS) 系统 + NUT (Network UPS Tools)

测试系统版本:0.9.12

本功能使用AI生成,lz只测试过ONBATT和ONLINE功能正常,其他需要自行测试,如有问题可发帖询问。能力有限,尽量解决问题

一、功能目标

  • 实时检测 UPS 事件(如断 电、恢复、低电量等)
  • 提取事件发生时间并发送带时间戳的通知邮件
  • 使用 HTML 格式美化邮件内容
  • 定期自动清理日志和状态文件
  • 避免重复通知旧事件(通过记录清理时间戳)

二、涉及组件

组件 路径 说明
监控脚本 /usr/local/bin/ups_event_monitor.sh 检测 syslog 中的 UPS 事件
通知脚本 /etc/nut/notifycmd 发送 HTML 邮件通知
日志文件 /var/log/ups_event_monitor.log 主要监控日志
状态文件 /tmp/ups_event_state/ 存储事件时间戳,防止重复通知
清理脚本 /usr/local/bin/cleanup_ups_logs.sh 清空日志并记录清理时间
定时任务 crontab -l 自动执行监控和清理

三、安装所需依赖

所需依赖一览

组件 用途 安装包名(OpenWrt) 安装包名(Debian/Ubuntu)
jq JSON 解析工具 jq jq
msmtp 邮件发送客户端 msmtp msmtp

使用以下命令检查是否安装所需依赖

# 1. 检查 jq 是否可用
jq --version
# 示例输出:jq-1.6
​
2. 检查 msmtp 是否可用
msmtp --version
# 示例输出:msmtp version 1.4.32

使用以下命令安装所需依赖

sudo apt update
​
# 安装 jq
sudo apt install jq -y
# 安装 msmtp
sudo apt install msmtp -y

设置 msmtp 配置文件

# 设置msmtp全局配置
sudo vim /etc/msmtprc
msmtp 配置文件 /etc/msmtprc
defaults
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
​
account default
# 填写 SMTP 服务器地址,比如使用qq邮箱发送邮件,这里就填:host smtp.qq.com
host smtp.host.com
port 587
auth on
# 收件人邮件地址
user your_email@example.com
# 密码/授权码,163、qq邮箱等使用授权码,可以上网搜索对应邮箱授权码如何获取
password your_password
# 发件人邮件地址,可与收件人邮件地址相同
from your_email@example.com

三、关键脚本说明

1. 通知脚本:/etc/nut/notifycmd

#!/bin/bash
​
# ===== 配置区 =====
SYSLOG_FILE="/var/log/syslog"
NOTIFY_SCRIPT="/etc/nut/notifycmd"
LOGFILE="/var/log/ups_event_monitor.log"
STATE_DIR="/tmp/ups_event_state"
mkdir -p "$STATE_DIR"
​
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" >> "$LOGFILE"
}
​
# 定义事件匹配规则(事件类型 => eventId => 消息)
declare -A EVENT_CONFIG=(
    ["ONBATT"]="UPS_ONBATT_LOWBATT 市电中断,已切换至电池供电"
    ["LOWBATT"]="UPS_BATTERY_LOW 电池电量低,请检查电源状态"
    ["FSD"]="UPS_FINAL_SHUTDOWN 即将关机,设备将在短时间内断 电"
    ["ONLINE"]="UPS_ONLINE 市电已恢复,UPS 正常运行"
    ["COMMBAD"]="UPS_COMM_LOST 与 UPS 的通信已中断,请检查连接"
    ["COMMOK"]="UPS_COMM_RESTORED 与 UPS 的通信已恢复"
)
​
# 遍历所有事件类型
for EVENT_TYPE in "${!EVENT_CONFIG[@]}"; do
    IFS=' ' read -r -a config <<< "${EVENT_CONFIG[$EVENT_TYPE]}"
    EVENT_ID="${config[0]}"
    DEFAULT_MSG="${config[1]}"
    
    # 提取最新一行含 EVENT_ID 的日志,并只保留 JSON 部分
    LOG_LINE=$(grep -a "$EVENT_ID" "$SYSLOG_FILE" | tail -n1)
​
    if [ -z "$LOG_LINE" ]; then
        log "$EVENT_TYPE" "❌ 未发现事件关键词"
        continue
    fi
​
    # 提取完整的 JSON 部分(从第一个 '{' 开始到最后)
    JSON_PART=$(echo "$LOG_LINE" | grep -o '{.*}')
​
    if [ -z "$JSON_PART" ] || ! echo "$JSON_PART" | jq . >/dev/null 2>&1; then
        log "$EVENT_TYPE" "⚠️ JSON 提取失败或无效"
        log "$EVENT_TYPE" "🔍 原始日志行: $LOG_LINE"
        continue
    fi
​
    NEW_EVENT_ID=$(echo "$JSON_PART" | jq -r '.eventId')
    NEW_EVENT_TIME=$(echo "$JSON_PART" | jq -r '.datetime')
​
    if [ "$NEW_EVENT_ID" != "$EVENT_ID" ]; then
        log "$EVENT_TYPE" "⚠️ eventId 不匹配: $NEW_EVENT_ID vs $EVENT_ID"
        continue
    fi
​
    if [ -z "$NEW_EVENT_TIME" ] || [ "$NEW_EVENT_TIME" = "null" ]; then
        log "$EVENT_TYPE" "⚠️ 无法提取时间戳"
        continue
    fi
​
    STATE_FILE="$STATE_DIR/last_event_$EVENT_TYPE.timestamp"
    if [ -f "$STATE_FILE" ]; then
        LAST_EVENT_TIME=$(cat "$STATE_FILE")
    else
        LAST_EVENT_TIME=0
    fi
​
    # 加载最后清理时间戳(如果存在)
    CLEANUP_TIME=0
    TIMESTAMP_FILE="$STATE_DIR/last_cleanup.timestamp"
    if [ -f "$TIMESTAMP_FILE" ]; then
        CLEANUP_TIME=$(cat "$TIMESTAMP_FILE")
    fi
​
    # 如果事件时间早于最后一次清理时间,则跳过
    if (( NEW_EVENT_TIME < CLEANUP_TIME )); then
        log "$EVENT_TYPE" "⏰ 事件发生在清理时间之前,跳过重复通知"
        continue
    fi
​
    if (( NEW_EVENT_TIME > LAST_EVENT_TIME )); then
        EVENT_TIME=$(date -d "@$NEW_EVENT_TIME" "+%Y-%m-%d %H:%M:%S")
        log "$EVENT_TYPE" "🔔 检测到新事件,时间戳: $NEW_EVENT_TIME ($EVENT_TIME)"
​
        NOTIFYTYPE="$EVENT_TYPE" \
        NOTIFYMSG="$DEFAULT_MSG" \
        NOTIFYDATETIME="$NEW_EVENT_TIME" \
            "$NOTIFY_SCRIPT"
​
        if [ $? -eq 0 ]; then
            log "$EVENT_TYPE" "✅ 成功发送通知"
            echo "$NEW_EVENT_TIME" > "$STATE_FILE"
        else
            log "$EVENT_TYPE" "❌ 通知执行失败"
        fi
    else
        log "$EVENT_TYPE" "⏳ 已处理过该事件,跳过重复通知"
    fi
done
admin@fnos:/vol3/1000/appdata/nginx-proxy-manager$ ^C
admin@fnos:/vol3/1000/appdata/nginx-proxy-manager$ cat /etc/nut/notifycmd
#!/bin/bash
​
# ===== 配置区 =====
EMAIL_RECIPIENT="17667192839@163.com"
LOGFILE="/var/log/nut/notifycmd.log"
​
# 创建日志目录和文件
mkdir -p /var/log/nut
touch "$LOGFILE"
​
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$NOTIFYTYPE] $1" >> "$LOGFILE"
}
​
# 将 Unix 时间戳转换为可读格式
if [ -n "$NOTIFYDATETIME" ]; then
    EVENT_TIME=$(date -d @"$NOTIFYDATETIME" "+%Y-%m-%d %H:%M:%S")
else
    EVENT_TIME=$(date '+%Y-%m-%d %H:%M:%S')
fi
​
TITLE="🚨 UPS 状态更新:$NOTIFYTYPE"
​
log "🔔 收到通知类型: $NOTIFYTYPE"
log "🕘 事件时间: $EVENT_TIME"
log "📝 消息内容: $NOTIFYMSG"
​
function send_email() {
    log "📧 正在尝试发送邮件到 $EMAIL_RECIPIENT"
​
    # 构建 HTML 内容
    HTML_CONTENT="
<html>
<head>
  <meta charset='UTF-8'>
  <title>UPS 状态通知</title>
</head>
<body style='font-family: Arial, sans-serif; background-color: #fafafa; padding: 20px;'>
  <div style='max-width: 600px; margin: auto; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); padding: 20px;'>
    <h2 style='color:#e67e22;'>🚨 UPS 状态更新:$NOTIFYTYPE</h2>
    <table border='0' cellpadding='8' cellspacing='0' width='100%' style='border-collapse: collapse;'>
      <tr><td style='background-color:#f2f2f2; width:30%;'><strong>事件类型:</strong></td><td>$NOTIFYTYPE</td></tr>
      <tr><td style='background-color:#f2f2f2;'><strong>事件时间:</strong></td><td>$EVENT_TIME</td></tr>
      <tr><td style='background-color:#f2f2f2;'><strong>事件描述:</strong></td><td>$NOTIFYMSG</td></tr>
      <tr><td style='background-color:#f2f2f2;'><strong>UPS 名称:</strong></td><td>$UPSNAME</td></tr>
      <tr><td style='background-color:#f2f2f2;'><strong>系统时间:</strong></td><td>$(date '+%Y-%m-%d %H:%M:%S')</td></tr>
    </table>
    <p style='margin-top:20px; color:#555;'>请及时处理。</p>
  </div>
</body>
</html>
"
​
    echo "$HTML_CONTENT" | msmtp "$EMAIL_RECIPIENT" <<EOF
To: $EMAIL_RECIPIENT
Subject: $TITLE
Content-Type: text/html; charset=UTF-8
​
$(cat)
EOF
​
    if [ $? -eq 0 ]; then
        log "✅ 邮件发送成功"
    else
        log "❌ 邮件发送失败 (msmtp error)"
    fi
}
​
case $NOTIFYTYPE in
    ONBATT|LOWBATT|FSD|ONLINE|COMMOK|COMMBAD)
        send_email
        ;;
    *)
        log "❓ 未知通知类型,未发送邮件"
        ;;
esac

功能:

  • 接收来自 NUT 的通知参数;
  • 构建 HTML 格式的邮件内容;
  • **使用 **msmtp 发送邮件;
  • **记录通知日志到 **/var/log/nut/notifycmd.log

示例邮件内容(HTML):

<h2 style="color:#e67e22;">🚨 UPS 状态更新:ONBATT</h2>
<table border="0" cellpadding="8" cellspacing="0" width="100%">
  <tr><td style="background-color:#f2f2f2;"><strong>事件类型:</strong></td><td>ONBATT</td></tr>
  <tr><td style="background-color:#f2f2f2;"><strong>事件时间:</strong></td><td>2025-06-30 00:05:51</td></tr>
  <tr><td style="background-color:#f2f2f2;"><strong>事件描述:</strong></td><td>市电中断,已切换至电池供电</td></tr>
  <tr><td style="background-color:#f2f2f2;"><strong>UPS 名称:</strong></td><td>ups1</td></tr>
</table>

2. 监控脚本:/usr/local/bin/ups_event_monitor.sh

#!/bin/bash
​
# ===== 配置区 =====
SYSLOG_FILE="/var/log/syslog"
NOTIFY_SCRIPT="/etc/nut/notifycmd"
LOGFILE="/var/log/ups_event_monitor.log"
STATE_DIR="/tmp/ups_event_state"
mkdir -p "$STATE_DIR"
​
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" >> "$LOGFILE"
}
​
# 定义事件匹配规则(事件类型 => eventId => 消息)
declare -A EVENT_CONFIG=(
    ["ONBATT"]="UPS_ONBATT_LOWBATT 市电中断,已切换至电池供电"
    ["LOWBATT"]="UPS_BATTERY_LOW 电池电量低,请检查电源状态"
    ["FSD"]="UPS_FINAL_SHUTDOWN 即将关机,设备将在短时间内断 电"
    ["ONLINE"]="UPS_ONLINE 市电已恢复,UPS 正常运行"
    ["COMMBAD"]="UPS_COMM_LOST 与 UPS 的通信已中断,请检查连接"
    ["COMMOK"]="UPS_COMM_RESTORED 与 UPS 的通信已恢复"
)
​
# 遍历所有事件类型
for EVENT_TYPE in "${!EVENT_CONFIG[@]}"; do
    IFS=' ' read -r -a config <<< "${EVENT_CONFIG[$EVENT_TYPE]}"
    EVENT_ID="${config[0]}"
    DEFAULT_MSG="${config[1]}"
    
    # 提取最新一行含 EVENT_ID 的日志,并只保留 JSON 部分
    LOG_LINE=$(grep -a "$EVENT_ID" "$SYSLOG_FILE" | tail -n1)
​
    if [ -z "$LOG_LINE" ]; then
        log "$EVENT_TYPE" "❌ 未发现事件关键词"
        continue
    fi
​
    # 提取完整的 JSON 部分(从第一个 '{' 开始到最后)
    JSON_PART=$(echo "$LOG_LINE" | grep -o '{.*}')
​
    if [ -z "$JSON_PART" ] || ! echo "$JSON_PART" | jq . >/dev/null 2>&1; then
        log "$EVENT_TYPE" "⚠️ JSON 提取失败或无效"
        log "$EVENT_TYPE" "🔍 原始日志行: $LOG_LINE"
        continue
    fi
​
    NEW_EVENT_ID=$(echo "$JSON_PART" | jq -r '.eventId')
    NEW_EVENT_TIME=$(echo "$JSON_PART" | jq -r '.datetime')
​
    if [ "$NEW_EVENT_ID" != "$EVENT_ID" ]; then
        log "$EVENT_TYPE" "⚠️ eventId 不匹配: $NEW_EVENT_ID vs $EVENT_ID"
        continue
    fi
​
    if [ -z "$NEW_EVENT_TIME" ] || [ "$NEW_EVENT_TIME" = "null" ]; then
        log "$EVENT_TYPE" "⚠️ 无法提取时间戳"
        continue
    fi
​
    STATE_FILE="$STATE_DIR/last_event_$EVENT_TYPE.timestamp"
    if [ -f "$STATE_FILE" ]; then
        LAST_EVENT_TIME=$(cat "$STATE_FILE")
    else
        LAST_EVENT_TIME=0
    fi
​
    # 加载最后清理时间戳(如果存在)
    CLEANUP_TIME=0
    TIMESTAMP_FILE="$STATE_DIR/last_cleanup.timestamp"
    if [ -f "$TIMESTAMP_FILE" ]; then
        CLEANUP_TIME=$(cat "$TIMESTAMP_FILE")
    fi
​
    # 如果事件时间早于最后一次清理时间,则跳过
    if (( NEW_EVENT_TIME < CLEANUP_TIME )); then
        log "$EVENT_TYPE" "⏰ 事件发生在清理时间之前,跳过重复通知"
        continue
    fi
​
    if (( NEW_EVENT_TIME > LAST_EVENT_TIME )); then
        EVENT_TIME=$(date -d "@$NEW_EVENT_TIME" "+%Y-%m-%d %H:%M:%S")
        log "$EVENT_TYPE" "🔔 检测到新事件,时间戳: $NEW_EVENT_TIME ($EVENT_TIME)"
​
        NOTIFYTYPE="$EVENT_TYPE" \
        NOTIFYMSG="$DEFAULT_MSG" \
        NOTIFYDATETIME="$NEW_EVENT_TIME" \
            "$NOTIFY_SCRIPT"
​
        if [ $? -eq 0 ]; then
            log "$EVENT_TYPE" "✅ 成功发送通知"
            echo "$NEW_EVENT_TIME" > "$STATE_FILE"
        else
            log "$EVENT_TYPE" "❌ 通知执行失败"
        fi
    else
        log "$EVENT_TYPE" "⏳ 已处理过该事件,跳过重复通知"
    fi
done

功能:

  • **解析 **/var/log/syslog 中的 JSON 格式事件;
  • **提取 **eventIddatetime
  • 若为新事件(时间大于上次记录),调用通知脚本;
  • 支持多种事件类型:ONBATT, LOWBATT, FSD, ONLINE, COMMBAD, COMMOK;
  • 忽略清理前发生的事件(避免误报);

示例日志输出:

2025-06-30 18:40:00 [ONBATT] 🔔 检测到新事件,时间戳: 1751213151 (2025-06-30 00:05:51)
2025-06-30 18:40:00 [ONBATT] ✅ 成功发送通知

3. 清理脚本:/usr/local/bin/cleanup_ups_logs.sh

#!/bin/bash
​
# ===== 配置区 =====
STATE_DIR="/tmp/ups_event_state"
LOG_FILES=(
    "/var/log/ups_event_monitor.log"
    "/var/log/nut/notifycmd.log"
    "/var/log/ups_*_monitor.log"
)
TIMESTAMP_FILE="/tmp/ups_event_state/last_cleanup.timestamp"
​
# 清空所有日志文件内容
for LOG_FILE in "${LOG_FILES[@]}"; do
    if [ -f "$LOG_FILE" ]; then
        > "$LOG_FILE"
        echo "✅ 已清空日志文件: $LOG_FILE"
    fi
done
​
# 删除状态文件(下次启动会自动重建)
if [ -d "$STATE_DIR" ]; then
    rm -f "$STATE_DIR"/*
    echo "✅ 已清空状态目录: $STATE_DIR"
fi
​
# 记录当前时间为最后清理时间
echo "$(date +%s)" > "$TIMESTAMP_FILE"
echo "✅ 已记录清理时间戳: $(date '+%Y-%m-%d %H:%M:%S')"

功能:

  • 清空所有相关日志文件内容;
  • 删除状态文件(避免重复通知);
  • 记录本次清理的时间戳,用于后续判断;
  • 支持定时自动运行;

示例输出:

✅ 已清空日志文件: /var/log/ups_event_monitor.log
✅ 已清空日志文件: /var/log/nut/notifycmd.log
✅ 已清空状态目录: /tmp/ups_event_state
✅ 已记录清理时间戳: 2025-06-30 00:00:00

四、定时任务设置

设置每天凌晨 2 点运行监控和清理脚本:

# 添加定时任务
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/ups_event_monitor.sh >> /var/log/ups_event_monitor_cron.log 2>&1") | crontab -
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/cleanup_ups_logs.sh >> /var/log/cleanup_ups.log 2>&1") | crontab -
​
# 查看定时任务
crontab -l

五、常用命令汇总

操作 命令
手动运行监控脚本 /usr/local/bin/ups_event_monitor.sh
手动运行清理脚本 /usr/local/bin/cleanup_ups_logs.sh
查看监控日志 tail -f /var/log/ups_event_monitor.log
查看通知日志 tail -f /var/log/nut/notifycmd.log
查看定时任务 crontab -l
设置脚本可执行权限 chmod +x /usr/local/bin/ups_event_monitor.sh chmod +x /etc/nut/notifycmd chmod +x /usr/local/bin/cleanup_ups_logs.sh

六、注意事项

注意项 说明
✅ 确保安装依赖 jq,msmtp,cron
✅ 不要删除 syslog 文件 否则会导致事件无法读取
✅ 清理脚本必须包含状态文件清理 否则会重复通知旧事件
✅ 修改脚本后务必测试运行 确保无语法错误
✅ 邮件测试可用手动触发 NOTIFYTYPE=ONBATT NOTIFYMSG="test" UPSNAME="ups1" /etc/nut/notifycmd
收藏
送赞
分享
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则