前言

大学以来,接触 Linux 命令行的时间越来越多,也越来越发现,在使用成品 NAS 系统的时候会遇到各种不便利。黑群晖毕竟是特殊手段运行的,用 U 盘启动缓慢且系统魔改而庞大;OpenMediaVault 虽然基于 Debian,但发现其运行的 PHP Web 服务在我本地运行的时候出现了各种疑难杂症,包括但不限于 WebUI 上应用配置的时候,经常需要等一两分钟的情况。抱着更高自定义,更低资源占用的目的,和学习命令行操作的心态,我决定直接为 NAS 安装原生 Debian 并自行部署服务。

本文章是该部署过程中,对不间断电源相关配置的记录。

由于时间仓促,文章疏漏在所难免,如果你发现有什么不对的地方,跑不起来之类的,可以大胆提出,说不定是文章问题,欢迎在评论区交流~

环境和主要软件

NUT

NUT (Network UPS Tools),顾名思义,具备网络功能的 UPS 工具。NUT 提供了非常多管理,控制和监控功能,可以为多 UPS,多设备的场景提供较为完善的管理支持。不过在本文,我们只讨论单独使用一个 UPS 为设备提供不间断电源的情况,只需要用到它的部分功能即可。

NUT 是一个 C/S(客户端/服务端) 架构的程序。这意味着,部署过程中,需要配置客户端部分和服务端部分。upsd 等充当服务端角色,向客户端提供 UPS 相关信息。upsmon 等充当客户端角色,监控并接收来自 upsd 提供的信息,并结合 upsd 提供的用户角色,对本机执行相关操作,并向服务端传递信息。

在本文运行场景下,客户端与服务端运行在同一个设备上。

安装

推荐使用 Debian 自带的 nut 软件包,他会将一系列运行需要的准备措施配置好。执行如下命令即可安装:

1
sudo apt install nut

需要注意的是,如果你参考了官方文档,你可能会注意到,有关 upsd 的指令会执行失败,提示大致形如下方:

1
2
3
4
5
6
7
8
youwenqwq@dango:~$ sudo upsd
Network UPS Tools upsd 2.8.0
fopen /run/nut/upsd.pid: No such file or directory
Could not find PID file '/run/nut/upsd.pid' to see if previous upsd instance is already running!

not listening on 127.0.0.1 port 3493
not listening on ::1 port 3493
no listening interface available

这是因为 Debian 软件包安装方式运行的 upsd 并不会创建 PID 文件,这些服务统一由 systemd 管理。upsd 在 systemd 中对应的服务是 nut-server。因此,需要重启 upsd 的话,执行 systemctl restart nut-server 或者更直接地,重启整个 nut.target 即可。

配置

注意:本文列出的配置文件,通常仅涉及需要修改的部分。其余保持默认即可。

所有配置文件均位于 /etc/nut 目录下,后续不做说明。

NUT 运行模式 (nut.conf)

在本文讨论范围内(即单 UPS 单主机)情况下,只需如下配置即可。

1
2
# /etc/nut/nut.conf
MODE=standalone

UPS 驱动 (ups.conf)

配置 UPS 驱动相对较为简单。通常情况下,以 root 权限运行 nut-scanner 即可自动扫描,获取到当前 UPS 的信息。示例的输出结果如下,可以看到,山特 TG-BOX 系列的驱动是 usbhid-ups

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
root@dango:/etc/nut# nut-scanner
Scanning USB bus.
No start IP, skipping NUT bus (old connect method)
Scanning NUT bus (avahi method).
Failed to create client: Daemon not running
[nutdev1]
        driver = "usbhid-ups"
        port = "auto"
        vendorid = "0463"
        productid = "FFFF"
        product = "SANTAK TG-BOX"
        serial = "Blank"
        vendor = "EATON"
        bus = "001"

通常情况下,复制从 [netdev1] 到最后的输出,然后粘贴到 /etc/nut/ups.conf 文件末尾即可直接使用。其中 [nutdev1] 为 UPS 名字,你可以自定义。我使用 [santek]

配置完成后,使用 systemctl restart nut.target 重启整个 nut 服务,然后可执行如下命令,检查 UPS 状态信息。其中,UPSNAME 修改为上述你自定义的 UPS 名字。

1
upsc UPSNAME@localhost

执行后,输出形如下方:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
root@dango:~# upsc santak@localhost
Init SSL without certificate database
battery.charge: 93
battery.charge.low: 20
battery.runtime: 1026
battery.type: PbAc
device.mfr: EATON
device.model: SANTAK TG-BOX 600
device.serial: Blank
device.type: ups
driver.name: usbhid-ups
driver.parameter.bus: 001
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
# 省略

upsd 用户 (upsd.users)

在文件末尾加上如下内容。

1
2
3
4
5
# /etc/nut/upsd.users

[USERNAME]
password=PASSWORD
upsmon primary

其中,USERNAME 改成你想要取的用户名,PASSWORD 改成你自定义的密码即可。

关于 upsmon primary,注释中有写,大致上就是为多设备情况下,调度电源策略使用。本文场景单设备,直接使用 primary (主服务器)即可。

本地运行无需修改 upsd.conf 的监听地址,因此无需动 upsd.conf,除非有需求。

upsmon 客户端配置 (upsmon.conf)

MONITOR 部分指定了 upsmon 客户端需要监控哪个 UPS 服务和设备。在 MONITOR 部分,加上如下配置

1
2
# MONITOR <system> <powervalue> <username> <password> ("primary"|"secondary")
MONITOR UPSNAME@localhost 1 USERNAME PASSWORD primary

其中修改 UPSNAME 为 ups.conf 中你自定义的名字。由于 upsd 在本地运行,因此主机名直接写 localhost 即可。USERNAME 和 PASSWORD 与上文设置的一致。通常情况下,无需更改 powervalue,保持为 1 即可,除非系统特殊。详见官方文档

如此设定之后,使用默认的配置即可实现低电量自动关机。你可以使用 systemctl restart nut.target 重启整个 nut 服务,应用更改的配置。随后,用如下命令进行低电量 Forced Shutdown 的模拟测试。

注意:这会执行 UPS 低电量时的整个关机流程,因此这会关闭你的 NAS 电源并重启你的 UPS。请不要在无法直接可靠接触到设备时操作,以免造成服务无法访问。

1
upsmon -c fsd

至此,整个 UPS 系统配置完毕。文末附录贴上了(稍微审核了一下没问题的)由 ChatGPT 翻译的 NUT 文档描述的关机流程,可供参考。

通知

UPS 系统能正常运作之后,我们通常还希望能及时了解到其状态的变化。在我的环境下,我用 邮件+ServerChan3 的提醒策略,既可以保证收到邮件,又能用 Server 酱及时在手机上收到提醒。

由于我只需要实现简单的提醒功能,因此不使用 upssched 进行更高级的配置,直接用 upsmon 自带的 NOTIFYCMD 即可。

由于只需要发送邮件提醒,而 sendmail 工具配置相对复杂,本文使用 msmtp。

安装

直接使用包管理器提供的安装包即可。

1
sudo apt install msmtp curl

编写邮件发件配置

msmtp 可以使用系统默认配置和用户配置。前者位于 /etc/msmtprc,后者位于 ~/.msmtprc$XDG_CONFIG_HOME/msmtp/config

本文采用系统配置。编辑 /etc/msmtprc,样例配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 设定账号名称,此处我取 default
account default

# 设定发件主机和端口
host examble.com
port 465

# 设定发件需要验证,并提供账号密码
auth on
user donotreply@welain.com
password YOURPASSWORD

# 开启 TLS
tls on
tls_starttls off

# 发件人地址
from donotreply@welain.com

配置完成后,可使用如下命令测试发件情况。(由于邮件信息不全,可能会被认为是 spam 而被放入垃圾箱,请检查)

1
echo "Helloworld" | msmtp youwenqwq@gmail.com

设定发件 & Server 酱脚本

由于 upsmon 的 NOTIFYCMD 将 NOTIFYMSG(下文会进行设置)内容通过字符串参数方式传递给 NOTIFYCMD 执行的指令,因此可以自定义一个脚本,读取字符串并进行切割处理,匹配到标题和正文进行发送。

示例脚本如下:

 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
#!/bin/bash

INPUT="$*"
SUBJECT="[dango] ${INPUT%%|*}" # "[dango] "为标题前缀
BODY="${INPUT#*|}"


# SMTP Mail
# 请修改 From 和 To

cat <<EOF | msmtp -t
From: nut@dango
To: youwenqwq@gmail.com
Subject: ${SUBJECT}

${BODY}
EOF

# ServerChan3
# 请修改 tags

json=$(cat <<EOF
{
  "title": "${SUBJECT}",
  "desp": "${BODY}",
  "tags": "UPS|NAS"
}
EOF
)

# 请修改 URL 为自己的 SendUrl

curl -X POST  https://XXXX.push.ft07.com/send/YOURTOKEN.send \
  -H "Content-Type: application/json" \
  -d "$json"

记得授予执行权限。

该脚本读取 NOTIFYMSG 内容,并识别其中的竖线分隔符("|"),将分隔符前面的内容识别为标题,后面的内容识别为正文;随后,通过 msmtp 和 curl 分别发送邮件和通知 ServerChan3。
如果你不需要 ServerChan3 通知,可以将 # ServerChan3 后面的部分全部删除。

设置 upsmon 触发通知

回到 /etc/nut/upsmon.conf。设置如下内容:

 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
# 右边替换为脚本存放位置
NOTIFYCMD /usr/local/bin/ups_notify.sh

# ...省略

# NOTIFYMSG <notify type> "message"

# 右侧文本竖线左边为标题,右边为正文。
# 似乎不支持中文,使用中文会无法显示。毕竟这些文本是要输出到 syslog 的。
# 如需使用中文及其他高级内容,可探索使用 upssched 发送通知。

NOTIFYMSG ONLINE        "AC Power Connected|UPS %s has been connected to AC Power"
NOTIFYMSG ONBATT        "AC Power Disconnected, using UPS battery|UPS %s is currently using battery"
NOTIFYMSG LOWBATT       "UPS Low Battery|UPS %s low battery!"
NOTIFYMSG FSD           "UPS Forcing shutdown|UPS %s is executing force shutdown"
NOTIFYMSG COMMOK        "Connection to UPS established|NAS has now connected to UPS %s"
NOTIFYMSG COMMBAD       "UPS disconnected|UPS %s has been disconnected from NAS"
NOTIFYMSG SHUTDOWN      "Shutting down|NAS is shutting down due to UPS low battery state"
NOTIFYMSG REPLBATT      "UPS battery needs replacement|UPS battery needs replacement!"
NOTIFYMSG NOCOMM        "UPS Unavailable|UPS %s is currently unavailable."

# ...省略

# NOTIFYFLAG <notify type> <flag>[+<flag>][+<flag>] ...

# 这里后面添加上 "+EXEC" 以执行 NOTIFYCMD

NOTIFYFLAG ONLINE       SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT       SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT      SYSLOG+WALL+EXEC
NOTIFYFLAG FSD          SYSLOG+WALL+EXEC
NOTIFYFLAG COMMOK       SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD      SYSLOG+WALL+EXEC
NOTIFYFLAG SHUTDOWN     SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT     SYSLOG+WALL+EXEC
NOTIFYFLAG NOCOMM       SYSLOG+WALL+EXEC
NOTIFYFLAG NOPARENT     SYSLOG+WALL+EXEC

通知部分到这里就结束啦,可以实现简单的提醒功能了~

最后

至此,简单的 UPS 配置和通知提醒就完成啦~ˋ( ° ▽、° )

参考资料


附录:NUT 关机流程设计(Shutdown Design)

当 UPS 电池电量不足时,操作系统需要被优雅地关闭。此外,UPS 的负载也需要断电,以便连接到 UPS 上的所有设备都能被强制重启,并在之后以可预测的顺序和状态重新启动,适应数据中心的需求。

以下是发生关键电源事件时的典型关机流程(以“一台 UPS 供电给一台或多台系统”的简单情况为例):

  1. UPS 切换至电池供电(on battery)

  2. UPS 电量降至低电(low battery)状态,成为“关键 UPS(critical UPS)”,即 upsc 显示如下状态:

    1
    
    ups.status: OB LB
    

    具体行为取决于 UPS 的型号,相关的参数包括:

    • battery.chargebattery.charge.low
    • battery.runtimebattery.runtime.low
  3. 主机上的 upsmon 进程检测到“关键 UPS”状态,并设置 FSD(强制关机标志,Forced Shutdown),通知所有次级系统(secondary)准备断电。

⚠️ 警告: 根据设计,为了确保所有系统能同时断电重启(而不是电力恢复时部分系统开机、部分仍关机),一旦 FSD 被设置,它将无法被取消,除非你重启 upsmon 进程。一旦进入关键电源模式,意味着我们打算完整执行整个流程——即优雅地关闭所有服务器,并最终关闭 UPS。

请注意:某些 UPS 设备及其驱动即使在“市电已恢复”后,也会继续保持 FSD 状态——只要电池电量未达到设备定义的安全阈值。这通常出现在“长时间停电后手动重启 UPS”的场景中。这是 UPS 厂商的设计策略,因为在这种情况下,如果再次停电,UPS 可能无法保证安全地关闭系统。因此,它们倾向于在电池未充满前保持关闭状态,以确保安全。

(如果你没有配置次级系统,直接跳至第 6 步) # 笔者注:本文环境无次级系统,直接跳到第 6 步。

  1. 所有次级系统的 upsmon 进程检测到 FSD,并执行以下操作:

    • 生成 NOTIFY_SHUTDOWN 事件
    • 等待 FINALDELAY 秒(默认通常为 5 秒)
    • 调用配置的 SHUTDOWNCMD
    • upsd 服务断开连接
  2. 主机(primary)系统等待最多 HOSTSYNC 秒(默认通常为 15 秒),以便让所有次级系统断开。如果在超时后仍有系统连接,主机将不再等待,继续执行自己的关机流程。

  3. 主机 upsmon 执行以下操作:

    • 生成 NOTIFY_SHUTDOWN 事件
    • 等待 FINALDELAY 秒(默认通常为 5 秒)
    • 在本地文件系统中创建 POWERDOWNFLAG 文件(通常是 /etc/killpower,或临时目录下的 /run/nut/killpower
    • 执行 SHUTDOWNCMD(通常是 shutdown -h now
  4. 在大多数系统上,init 进程接管后会:

    • 终止所有进程
    • 同步并卸载部分文件系统
    • 将部分文件系统重新挂载为只读
  5. 接着 init 执行你的关机脚本。该脚本会检查 POWERDOWNFLAG,如果存在,就通知 UPS 驱动,向 UPS 发送断电命令,关闭 UPS 输出电源。

  6. 所有系统彻底失去电力。

  7. 时间过去,市电恢复,UPS 自动重新供电。

  8. 所有系统重启并恢复正常运行。