群辉连通UPS控制Esxi断电关机

之前一直只是单纯的把UPS作为稳压器使用,由于停电频率太低,半年才停了一次电,但停电之后的突然掉电导致我的HomeAssistantOS虚拟机的虚拟硬盘直接故障了,这次就来把UPS设置一下。

UPS简述

UPS(Uninterruptible Power Supply)不间断电源,一般用于稳定电压或防止突然掉电导致电子设备损坏。这里我使用的UPS型号是雷迪斯H1000M,有一个USB口用于UPS网络通知。因此我可以通过USB先连接群辉NAS而后使用群辉对Esxi服务器进行通知。

设置群辉UPS服务器

Step1 连接USB线

使用USB线将UPS网络通知口与群辉进行连接:

Step2 检查UPS状态

使用管理员权限登入群辉DSM,从控制面板中选择”高级模式=>硬件和电源=>不断电系统”:

点击设备信息,查看UPS是否连接成功:

Step3 DSM面板上UPS设置

启用UPS支持:

这里说明一下”进入安全模式之前的等待时间“,这里我理解为如果超过设置的时间供电还没有恢复则关机。

因为我们需要群辉通知Esxi服务器,因此这里需要启用网络UPS服务器,下面的列表中填入需要进行关机的电子设备的ip地址:

我这里就只填了我两个Esxi服务的IP地址,如果你有其他设备,比如PC机、树莓派等也连接在同一个UPS下也可以填入。

Step4 通过SSH进入群辉进行UPS服务密码设置

首先是需要开启群辉SSH功能:

而后使用Administrator权限的账户进行登入:

这里我们需要将ups服务密码修改一下,进入UPS配置文件夹:

cd /usr/syno/etc/ups

然后需要修改 upsd.users文件,将其中的密码更改一下(注意这里需要sudo使用root权限执行):

需要注意的是如果修改了这里的monuser的password,则需要将群辉 /usr/syno/etc/ups/upsmon.users 进行修改:

我这里又一个比较奇怪的事情,我将这里修改后,群辉每次重启都会把我这里的设置给覆盖掉,导致群辉上的upsmon一直无法连接到upsd上。将这里也修改正确后,使用upsd -c reload 将UPS服务器重启(重新加载配置文件),使用upsmon将群辉ups监控服务重启,然后使用upsc -c 可以查看当前连接到UPS服务器上的客户机IP地址:

ash-4.3# upsd -c reload
Network UPS Tools upsd DSM6-2-25510-201118
ash-4.3# upsmon -c reload
Network UPS Tools upsmon DSM6-2-25510-201118
ash-4.3# upsc -c ups@localhost 
127.0.0.1

如果这里没有出现 127.0.0.1 则需要检查upsmon.conf中是 MONITOR 行是否设置正确,或者 upsd.users 密码设置错误。修改后需重新进行以上服务重启指令。

到这里如果你只有一个群辉机器使用UPS就可以结束了,下面是到这里设置为止的断电流程:

  • AC供电断开
  • 群辉UPS状态轮询得到ups.status=OB(OnBattery),写入Server is on battery日志,并弹出提示
  • 在设定的 DiskStation 进入 “安全模式” 之前的等待时间后(可能1~2s误差),群辉开始进入安全模式(广播ssh用户)
    • 广播ssh用户消息:服务器现在将会关闭
  • 群辉服务器卸载硬盘,停止服务,进入安全模式
  • 群辉在进入安全模式后向UPS发出shutdown(关闭)指令
  • UPS断开后部电源,群辉断电
  • UPS在1~2mins后彻底关闭

如果设置“进入安全模式之前等待时间”过长,UPS内部电池将一直被消耗,直到达到一个临界值 ups.battery.low(有的UPS可以设置,默认好像是10.3%),而后置ups.status=OB LB(LowBattery),强制触发群辉进入安全模式,接着从上方流程中第三步开始执行。

如果没有设置 “当系统进入安全模式时将不断电系统关机”,则在群辉进入安全模式后UPS将不会被关机,有些UPS在AC供电断开情况下蜂鸣器会持续发出警报,因此更加推荐勾选该设置。

注意:在此处只使用DSM设置面板中不断电系统进行UPS设置时,群辉实际上只是进入了安全模式,这里介绍一下安全模式是什么意思,安全模式下,群辉将会把所有服务关闭,同时卸载硬盘,在这种情况下直接断开电源将不会对硬盘等设备造成损伤。

it stops all services and unmounts volumes in order to prevent data loss and shut down (halt for EDS14) safely when the UPS device runs out of power. 

如果你只有一个群辉需要使用UPS,则到这里你就已经完成了你的目标,下面是对UPS工具和服务的深入和对群辉UPS脚本的改造

设置其他使用该UPS的服务器的前提

其他使用该UPS的服务器若需要群辉在停电时通知他们进行关机,则需要首先把他们置于同一个网络下,而且这个网络下的网络设备也必须接入这个UPS,保证网络的畅通。而后群辉和服务器都应该接在一个或一组UPS上。

比如我这里就是群辉和ESXi服务器接在同一个交换机上,交换机的电源也接在UPS上,这样就保证了在停电情况下服务器和UPS可以继续通信。

设置Esxi前需要的知识

群辉UPS不断电系统实际使用的是NUT(Network UPS Tools)作为内部服务,因此如果我们想要对整个UPS执行流程进行自定义修改,则需要NUT相关的知识,下面我简单介绍一下NUT所包含的服务、系统和架构

NUT架构

UPS驱动服务

这里我们从源头开始出发,首先我们需要一个服务与UPS进行直接通讯,这个服务实际使用了UPS的驱动,通过驱动与UPS进行通讯,而这个服务在NUT所暴露出来的命令在群辉中叫 /usr/bin/upsdrvctl,而其配置文件为ups.conf,群辉UPS默认名为ups。

ash-4.3# /usr/bin/upsdrvctl -h
Network UPS Tools - UPS driver controller DSM6-2-25510-201118
Starts and stops UPS drivers via ups.conf.

usage: /usr/bin/upsdrvctl [OPTIONS] (start | stop | shutdown) [<ups>]

  -h			display this help
  -x			pass [val=value] to driver
  -r <path>		drivers will chroot to <path>
  -t			testing mode - prints actions without doing them
  -u <user>		drivers started will switch from root to <user>
  -D            	raise debugging level
  start			start all UPS drivers in ups.conf
  start	<ups>		only start driver for UPS <ups>
  stop			stop all UPS drivers in ups.conf
  stop <ups>		only stop driver for UPS <ups>
  shutdown		shutdown all UPS drivers in ups.conf
  shutdown <ups>	only shutdown UPS <ups>

UPS Web服务 UPS Information Server

我们有与UPS进行通讯的服务后,需要注意我们的目标是为网络环境下提供UPS服务,因此我们需要一个Web服务器提供UPS状态等查询服务,而这部分在NUT中叫做/usr/sbin/upsd,而其配置文件为upsd.conf。而UPS服务是对于整个系统非常重要的一环,不能让没有权限的用户进行访问,因此我们需要设置账户和密码,而账户和密码在群辉上upsd.users文件中进行配置。

ash-4.3# /usr/sbin/upsd -h
Network UPS Tools upsd DSM6-2-25510-201118
Network server for UPS data.

usage: upsd [OPTIONS]

  -c <command>	send <command> via signal to background process
		commands:
		 - reload: reread configuration files
		 - stop: stop process and exit
  -D		raise debugging level
  -h		display this help
  -r <dir>	chroots to <dir>
  -q		raise log level threshold
  -u <user>	switch to <user> (if started as root)
  -V		display the version of this software
  -4		IPv4 only
  -6		IPv6 only

If you don’t change it with configure or this value, upsd will listen on port 3493 for this interface.

这里再重新介绍一下upsd.users配置文件:

upsd.users

upsd.users配置文件中可以通过配置upsd准许接入的账户及其权限达到UPS系统安全性的目的。下面中括号中monuser是群辉upsd.users默认提供的一个账户,该账户的模式是master模式(后文有master模式具体讲解),而登入该账户的密码为secrect。在群辉该文件中拥有详细的权限配置说明,可以通过设置限制对应账户下用户的读、操作权限。

# MONITOR myups@localhost 1 upsmon pass master  (or slave)
[monuser]
        password = secret
        upsmon master

UPS 监控服务

我们注意到上面的UPS Web服务只提供了单独的Web服务功能,而实际我们需要一个Consume该Web服务的另一个监控类型的服务,这个监控服务将会持续轮询UPS状态,根据UPS状态进行进一步的任务触发。而这个监控服务叫upsmon,在群辉上/usr/sbin/upsmon位置,其配置文件为upsmon.conf。这文件中将会指定监控的目标,即监控哪一个upsd,同时在其中指定UPS状态变化时需要执行的脚本。

ash-4.3# /usr/sbin/upsmon -h
Network UPS Tools upsmon DSM6-2-25510-201118
Monitors UPS servers and may initiate shutdown if necessary.

usage: /usr/sbin/upsmon [OPTIONS]

  -c <cmd>	send command to running process
		commands:
		 - fsd: shutdown all master UPSes (use with caution)
		 - reload: reread configuration
		 - stop: stop monitoring and exit
  -D		raise debugging level
  -h		display this help
  -K		checks POWERDOWNFLAG, sets exit code to 0 if set
  -p		always run privileged (disable privileged parent)
  -u <user>	run child as user <user> (ignored when using -p)
  -4		IPv4 only
  -6		IPv6 only
  -b		System on booting

UPS状态变化计时器帮助程序

NUT官方为我们提供了一个方便的软件upssched(UPS Scheduler),这个软件为我们提供了在UPS状态变化时设置一个倒计时Timer的能力,当Timer到期后将会把Timer 名称传入我们自定义脚本中,同时在Timer计时期间可通过UPS状态变化将其取消,例如设置在OB(OnBatter)下名为shutdown的Timer 15s后执行关机程序,在OL(OnLine,AC供电接入状态)下将该具名Timer(shutdown)取消。这个程序为我们提供了极大的便利。

upssched – Timer helper for scheduling events from upsmon

upssched was created to allow users to execute programs at times relative to events being monitored by upsmon(8). The original purpose was to allow for a shutdown to occur after some fixed period on battery, but there are other uses that are possible.

NUT文档

以上就是对架构中各服务的简述。

架构简图

简单画个图:

上图有一个没有讲到的知识点,每一个方框都是一个服务器,左上角分为Master和Slave两种类型,这两种在upsmon.conf MONITOR行进行设置,master模式下该服务器将会在所有slave断开与upsd连接后关闭,slave模式下的服务器将会按照正常流程关闭。同时这里可以很清晰的看到UPS状态数据的流向:upsdrvctl=>upsd=>(web服务: 192.168.0.10:3493)=>upsmon==传递状态变化OB/LB/OL==>upssched==设置具名timer:hello_timer、timer超时将timer名传递==>自定义脚本,这里读取到入参为hello_timer=>自定义脚本内部执行或者调用poweroff等指令。

需要注意的是这里的upsmon读取upsd的方法是通过轮询,具体轮询方面参数需要在upsmon.conf中进行修改。

辅助工具

上面我们就基本将NUT的架构理清楚了,下面我们介绍一个命令行使用的ups客户端:upsc(UPS Client)。

ash-4.3# upsc -h
Network UPS Tools upsc DSM6-2-25510-201118

usage: upsc -l | -L [<hostname>[:port]]
       upsc <ups> [<variable>]
       upsc -c <ups>

Demo program to display UPS variables.

First form (lists UPSes):
  -l         - lists each UPS on <hostname>, one per line.
  -L         - lists each UPS followed by its description (from ups.conf).
               Default hostname: localhost

Second form (lists variables and values):
  <ups>      - upsd server, <upsname>[@<hostname>[:<port>]] form
  <variable> - optional, display this variable only.
               Default: list all variables for <host>

Third form (lists clients connected to a device):
  -c         - lists each client connected on <ups>, one per line.
  <ups>      - upsd server, <upsname>[@<hostname>[:<port>]] form

可以看到上面有三种用法,但后面两种需要一个叫<ups>的参数,查看下面对于该参数的描述,我们可以得出在群辉中访问群辉的upsd需要使用ups@localhost(不能填IP,暂时不知道为什么,由于我通过文档描述的hosts.allow也没有找到具体的准入ip列表),下面我们执行以下这三种指令类型:

ash-4.3# vi  /etc/hosts.allow
ash-4.3# upsc -l localhost
ups
ash-4.3# upsc ups@localhost
battery.charge: 100
........
ups.status: OL TRIM
ups.type: offline / line interactive
ash-4.3# upsc -c ups@localhost
xxxxxxxx
xxxxxxxx
127.0.0.1

输出还是比较清晰的,这里就不多赘述了。

到这里为止我们就把NUT架构方面所需要的知识基本了解了,然后需要了解的是NUT的执行流程。

NUT执行流程

这里是直接对NUT文档中ShutdownDesign的一个翻译:

当UPS电池电量较低时,操作系统需要被安全的进行关闭。同时为了在UPS记入AC供电时设置来电启动(Forcibly Reboot)的设备自动启动需要将UPS关闭。

下面是当重要的能源事件(这里是UPS低电量时)发生时的NUT的执行步骤:

  1. UPS与AC供电断开,处于内部电池供电状态
  2. UPS内部电池电量达到低电量状态
    1. 此时通过upsc读取UPS参数,其中ups.status应为 “OB LB”即OnBattery LowBattery
    2. 低电量状态的触发临界量由UPS型号不同而不同,但都下述参数相关:
      1. battery.charge battery.charge.low
      2. battery.runtime battery.runtime.low
  3. Master模式下的upsmon服务发出提醒并设置FSD(Force Shutdown)标志,该标志位将告诉(不是通知,因为需要Slave模式的系统自己轮询)所有Slave模式的upsmon尽快降低电源负载(关机、加入低耗电模式、进入安全模式)。如果你没有Slave模式的upsmon,跳到第六步。
  4. Slave模式下的upsmon看见FSD标志
    1. 生成 NOTIFY_SHUTDOWN 事件 (这个是用于通知用户,比如发邮件短信等等)
    2. 等待 FINALDELAY所设置的时间,默认为5s。
    3. 执行他们upsmon.conf设置中SHUTDOWNCMD指令。
    4. 断开与upsd的链接
  5. Master模式下的upsmon将等待HOSTSYNC(默认15)秒用于等待所有Slave模式系统与upsd断开连接。如果超过HOSTSYNC时间Master将会停止等待,执行自己的Shutdown程序。
  6. Master模式下的upsmon将会:
    1. 生成NOTIFY_SHUTDOWN事件
    2. 等待 FINALDELAY所设置的时间,默认为5s。
    3. 创建POWERDOWNFLAG文件,通常为/etc/killpower
    4. 执行他们upsmon.conf设置中SHUTDOWNCMD指令。
  7. 大部分系统中,init进程将会接管控制权,停止所有进程,卸载文件系统,等等。
  8. init进程然后会执行你的shutdown脚本。这一步将会检查POWERDOWNFLAG文件,而后告诉UPS驱动关闭UPS。
  9. UPS断开后端供电,所有后部系统失去供电
  10. UPS在等待一段时间后(1min左右)彻底关闭。
  11. 一段时间过去,电力恢复,UPS接入AC供电,UPS重新上线。
  12. 所有后部系统由于供电恢复重新开机继续工作。

上面就是整个NUT的执行流程。这里我们可以看到,默认情况下NUT只会在UPS电量低时才会开始关闭后部系统中的设备。即只有当LB状态出现时,NUT才会开始通过Master设置FSD然后Shutdown。

这里我们可以看到实际上关闭系统这个通知是通过FSD标志执行,因此如果我们能手动控制FSD标志的设置时间就可以为我们提供最大的便利,而NUT也就是这样做的,在UPS低电量前我们可以通过具有权限的upsmon服务,执行 upsmon -c fsd 强行设置FSD标志,达到切入执行流的目的。

UPSMON NOTIFYFLAG 类型

NOTIFYFLAG ONLINE EXEC
NOTIFYFLAG ONBATT EXEC
NOTIFYFLAG LOWBATT EXEC
NOTIFYFLAG NOCOMM EXEC
NOTIFYFLAG COMMBAD IGNORE
NOTIFYFLAG COMMOK IGNORE
NOTIFYFLAG SHUTDOWN IGNORE
NOTIFYFLAG FSD EXEC
NOTIFYFLAG NOPARENT SYSLOG

这里我们来了解一下upsmon中NOTIFYFLAG(这里我以群辉上的版本为准,NUT文档中命令为NOTIFYMSG)后跟的状态命令值与UPS状态的具体关联:

  • ONLINE
    • UPS重新上线,就是开始AC供电,电力恢复
  • ONBATT
    • UPS开始消耗内部电池,停电了
  • LOWBATT
    • UPS内部电池电量低
  • FSD
    • ForcedShutdown UPS被Master模式upsmon强制关闭
  • COMMOK
    • 与UPS通讯恢复
  • COMMBAD
    • 与UPS通讯丢失
  • SHUTDOWN
    • 系统将被Shutdown
  • REPLBATT
    • UPS电池需要更换
  • NOCOMM
    • UPS无法交流,无法监控(不是很清楚)
#upsmon.conf
....
NOTIFYCMD /usr/sbin/upssched
......
NOTIFYFLAG ONLINE EXEC

这里可以看到在ONLINE状态后还接了一个EXEC,实际上这里是指将”ONLINE”作为参数传入上面设置的NOTIFYCMD指令中,如果这里是IGNORE就是忽略这个Action,如果为WALL是通知本机所有用户,如果是SYSLOG则记录到日志上。这里以upssched为例,upssched将会接收到参数ONLINE而upssched的执行将会由upssched.conf决定:

#upssched.conf
.....
CMDSCRIPT /usr/syno/bin/synoups
.....
AT ONLINE * EXECUTE online
AT ONLINE * CANCEL-TIMER fsd
AT LOWBATT * EXECUTE lowbatt
AT NOCOMM * EXECUTE nocomm
AT FSD * EXECUTE fsd
AT ONBATT * EXECUTE onbatt
AT ONBATT * START-TIMER fsd 15

直接以群辉上的配置文件为例子。这里可以看到有一系列的AT命令。这个命令也比较好理解:

在(AT) 状态(ONLINE/LOWBATT) UPS名称(*所有、ups) 时 [执行|设置Timer|取消Timer] [入参名|Timer名]

这里在Timer超时或者直接EXECUTE时将会把Timer名或者EXECUTE后面的参数传入上方设置的CMDSCRIPT中,这里就是群辉的/usr/syno/bin/synoups

理解群辉魔改的流程

群辉实际上并没有走完NUT默认设置的流程,而是通过upssched和自定义脚本在ONBATT触发时就开始设置名为fsd的计时器并开始计时,如果fsd计时器超时时仍然没有恢复电力,则开始执行自定义脚本入参为fsd的分支。如果fsd计时执行时恢复了电力则取消计时器执行。而下面这一行代码中最后的数字也是我们通过DSM中不断电系统设置的 进入安全模式之前的等待时间 实际填写的位置。我们每一次更改这个时间,群辉就会覆盖一次这个文件(但我无法通过修改/usr/syno/etc.defaults/ups/中的文件来持久化我的设置,后面可以尝试改下文件权限试试):

AT ONBATT * START-TIMER fsd 15 #这个15就是 进入安全模式之前的等待时间

因此我们可以有上面的所有知识得出结论,群辉在UPS外部供电断开后就开始计时,而后在这里设置的时间左右开始自己关机,如果设置了进入安全模式关闭UPS,则会在群辉进入安全模式后立即关闭UPS,导致整个后部系统断电。然后重点就来了:

我们的目的是什么?是让群辉进入安全模式同时通知其他服务器,其他服务器全部关机或进入安全模式后群辉再将UPS的电源断开,以便来电自动开机。

所以我们需要针对群辉的/usr/syno/bin/synoups进行自定义魔改,让群辉等待服务器关机后再ShutdownUPS。

群辉UPS脚本 synoups

这里我们先猜测一下这个synoups里面应该是什么,首先他接收online/onbatt/fsd等参数,而后根据不同参数执行不同程序,而这个sh文件肯定只有一个入口,因此我们很容易猜到内部是一个switch结构,将入参通过switch 分派到不同的分支程序上执行不同的代码。下面是synoups内这个switch结构:

case "$1" in
online)
    SynoUpsStateLog "synoups online"
    UPSRestart
    ;;
onbatt)
    SynoUpsStateLog "synoups onbatt"
    ONBatt
    ;;
lowbatt)
    SynoUpsStateLog "synoups lowbatt"
    UPSSafeMode $1
    ;;
nocomm)
    SynoUpsStateLog "synoups nocomm"
    UPSSafeMode $1
    ;;
fsd)
    SynoUpsStateLog "synoups fsd"
    /usr/sbin/upsmon -c fsd
    UPSSafeMode $1
    ;;
shutdownups)
    SynoUpsStateLog "synoups shutdownups"
    UPSShutdown
    ;;
statelog)
    SynoUpsStateLog "synoups statelog"
    ;;
esac

可以看到这是一个以入参作为分支标志的switch结构,而我们最关注的第一个是onbatt第二个是fsd。

这里我可以简单说一下ONBATT函数做了什么,他创建了一个表明ups在使用battery的文件/tmp/ups.onbatt,然后记录了下日志。然后就什么都没做了。

fsd分支中比较重要的是,他先通过 /usr/sbin/upsmon -c fsd 向UPS状态中添加FSD,表明UPS将要被Shutdown了,然后再执行了UPSSafeMode。UPSSafeMode是设置群辉进入安全模式的一个函数。然后就返回了。

如果你设置了进入安全模式后关闭UPS,是不是感觉没头没尾,因为fsd分支内没有关于关闭UPS的代码,这里实际上是在shutdownups分支中。但是我也不知道哪里调用了这个分支,我猜测是在确认群辉进入了安全模式后某个回调脚本中执行了/usr/syno/bin/synoups shutdownups

shutdownups分支中调用了一个函数UPSShutdown,这个函数就是将UPS关闭的函数。下面是UPSShutdown的代码,这里我简单解释一下,首先这里判断了下群辉自己是不是Master模式,如果是再继续执行,然后进入一个循环,每十秒确认一次UPS的状态。若发现有两次确认UPS都处于OL状态则直接中断执行然后返回,如果两次都在LB低电量状态,则开始尝试关闭UPS, /usr/bin/upsdrvctl shutdown 指令可以通过驱动与UPS沟通使其关闭。这里进行了失败重试,若关闭UPS三次都失败了就返回,每次间隔40s。

UPSShutdown() {
	if [ $UPSMaster -ne 1 ]; then
		touch /var/.NormalShutdown
		SYSLOG "UPS waits for safe shutdown."
		return
	fi
	OL=0;LB=0
	while [ $OL -ne 2 -a $LB -ne 2 ]; do
		sleep 10
		St=`UPSStatusGet`
		if [ "$St" = "OL" ]; then
			OL=`expr $OL + 1`;LB=0
		else
			LB=`expr $LB + 1`;OL=0
		fi
		echo "OL=$OL LB=$LB" >> $SZF_SAFEMODE
	done
	if [ $OL -eq 2 ]; then
		synologset1 sys warn 0x11300012
		$SYNOBOOTBIN --unset-safe-shutdown
		telinit 6
	elif [ $LB -eq 2 ]; then
		touch /var/.NormalShutdown
		if [ $UPSSafeShutdown -eq 0 ]; then
			SYSLOG "Waiting UPS exhausted."
		else
			shutdown_retry=0
			while [ $shutdown_retry -ne 3 ]; do
				StopUps
				SendMsg "StartStopUPS_by_upsdrvctl"
				/usr/bin/upsdrvctl shutdown
				if [ $? -eq 0 ]; then
					return 0
				fi
				sleep 40
			done
		fi
	fi
}

这里我们就可以把群辉内部脚本的执行理清楚了,首先在ONBATT时,设置fsd计时器,超时则执行synoups脚本fsd分支。fsd分支触发upsmon -c fsd,通知其他slave系统shutdown,而后群辉开始进入SafeMode。进入SafeMode后,群辉执行synoups upsshutdown分支,然后关闭UPS。

这里我们需要切入的是在群辉设置FSD标志后等待其他服务器关闭后再进入SafeMode,然后关闭UPS。因此这里就比较清晰了,我们需要在synoups上的fsd分支动手脚

设计synoups魔改内容

通过上面的分析我们得出,需要对fsd分支进行魔改达到我们的目的。

这里我们开始思考,如何知道我们的其他系统是否关机呢?其实upsc早已为我们提供了这个api,直接 upsc -c ups@localhost 我们就可以得到当前upsd上连接的upsmon,然后我们只需要把群辉本机 127.0.0.1 除开,如果其他的链接都不存在,则说明其他的服务器已经关闭,我们就可以放心的关闭UPS了。

注意:

我们需要注意的是这里实际上判断还是有一点问题的,如果其他服务器先将链接断开后再去做其他shutdown之前的比较耗时准备工作,那可能群辉一查到没有其他服务器连接就关闭UPS,导致本在执行shutdown准备工作的服务器直接掉电关机数据丢失,这里我直接这样写是有应用场景的原因的。我的Slave是两个Esxi服务器,他们即时设置了sequence shutdown vm都耗费不了多少时间,而且他们是先关VM后断连接,所以没有这个影响。但如果你的从机也是自己魔改的,就需要注意这个问题。

ash-4.3# upsc -c ups@localhost
xxx.xxx.xxx.xxx
xxx.xxx.xxx.xxx
127.0.0.1

这里我们来设计一下这个等待其他服务器关机的函数的流程:

  • 进入函数
  • 循环次数=50
  • 连接循环: (这是一个标签,后面JUMP到这里来)
  • CLIENTS=当前连接列表(即上面例子中三行IP)
  • if CLIENTS 只有 127.0.0.1
    • 关闭UPS
  • elseif CLIENTS 为空 (群辉的upsmon出问题了)
    • 关闭UPS
  • 程序休眠5s
  • if 循环次数 > 0
    • JUMP 连接循环 (从 连接循环 标签处执行)
  • 关闭UPS

上面就是整个流程的伪代码,这里我们没有考虑在这途中电力恢复的情况,因为在我应用场景下,只要Esxi服务器开始关机是不能中断的,就直接继续执行了,但我代码里面是写了UPS状态OL则中断执行。

编写WaitServersDisconnect函数

这一步推荐拥有一定Linux基础和Shell基础的同学进行操作,如果没有一定Shell基础,是很容易将文件改错,导致整个群辉UPS功能无法正确执行。同时修改之前一定要将原始的synoups进行备份

这里我将上面的逻辑放到了一个名为WaitServersDisconnect的函数中:

# $1:wait loop count,wait util=$1 * 5s  
# $2:test flag.just for test.don't shutdown ups while debugging
WaitServersDisconnect(){
    local CLIENT=$(${BIN_UPSC} -c "ups@${UPSMonServer}" | sed ':a;N;$!ba;s/\n/,/g' 2>/dev/null)
    local Index=$1
    HAS_CLIENT=`expr length "$CLIENT"`
    SendMsg "${Index}:(${CLIENT})"
    while [ $HAS_CLIENT -gt 5 ] && [ $Index -gt 0 ]
    do
        sleep 5
        IS_ONLINE=$(${BIN_UPSC} "ups@${UPSMonServer}" ups.status)
        if [[ "$IS_ONLINE" == *"OL"* ]]
        then
            echo "UPS is ONLINE"
            SendMsg "UPS_is_Online"
            return
        fi
        if [ "$CLIENT" = "127.0.0.1" ]
        then
            break
        fi
        SendMsg "${Index}:(${CLIENT})"
        CLIENT=$(${BIN_UPSC} -c "ups@${UPSMonServer}" | sed ':a;N;$!ba;s/\n/,/g' 2>/dev/null)
        HAS_CLIENT=`expr length "$CLIENT"`
        Index=$(($Index-1))
    done
    sleep 5
    if [ "$2" = "test" ]
    then
     echo "UPSShutdown"
    else
        SendMsg "WAIT_SERVER_UPSShutdown:$Index:($CLIENT)"
        UPSShutdown
    fi
}

这里我把很多调试用的echo和我写的一个发送消息到我的日志服务器的函数给删了,只剩最核心的代码。下面我们来一句一句解释,

1.首先是第一句CLIENT赋值。BIN_UPSC很明显存储upsc绝对路径的一个变量(这里的变量定义在synoups同级目录下的synoupscommon文件中,synoups最开始引用了它),UPSMonServer是synoups文件中定义的群辉upsmon所监控的目标服务器地址,这里就直接使用他们了。这里实际执行了:

/usr/bin/upsc -c ups@localhost

然后通过一个管道将结果传入了一个sed命令中,这个sed命令将多行的IP中的\n 替换为逗号,其实就是把多行IP变为单行,然后存储在CLIENT变量中。在本节最后我将解释一下这个sed命令的构成和原因。

2.Index用于标明当前最大等待时间,函数最大等待时间为 $1 * 5s=Index * 5s 即传入的第一个参数若为50,则最长等待时间为50*5=250s。

3.HAS_CLIENT用于计算CLIENT字符串长度,这里写的比较随便。如果HAS_CLIENT数值大于5,我就架设他还有没有断开的Slave服务器。

4.然后是一个While循环,循环条件为:CLIENT字符串长度大于5 同时 Index 大于 0

while循环头部:

5.休眠5秒

6.检查UPS状态是否OL,如果OL则终止执行(但在我应用场景没用,因为Esxi不会中断关机,还不如让他关了重启)

7.检查CLIENT字符串若只有127.0.0.1,则跳出循环。即只有群辉本机连着upsd,说明其他服务器都关了。

8.重新获取当前连接到upsd的client列表

9.重新计算CLIENT长度

10. Index自减

while循环尾部

11.休眠5秒

12.关闭UPS

函数结束

注意:

这里对于CLIENT的sed处理其实不是必须的,我这里是为了将结果传入我自定义的SendMsg命令中,由HTTP请求中的querystring传到我的一个测试服务器上,所这里用sed命令将多行的IP中的\n替换为逗号。这样我就可以直接把CLIENT变量值作为querystring参数值传走了。

sed指令

这里实际上从逻辑上来说是用一个当到文件尾行跳出的while循环读取输入然后追加到pattern buffer上。出循环后再将pattern buffer中的换行符替换为逗号。

sed ':a;N;$!ba;s/\n/,/g'
#可以理解为下面的伪代码:
:a;  #一个名为a的标签
N;   #读取下一行并追加到一个叫pattern buffer的缓存中
$!   #如果不是文件尾部(最后一行),则执行下一行
  ba; #emm就是跳到branch a,其实是跳转到上面的a标签
s/\n/,/g #将"\n"替换为"," 且为全局替换

修改fsd分支

上面把WaitServersDisconnect函数编写好了,下面我们需要将这个函数接入到synoups执行流程中,这里我们需要将该函数插入fsd分支末尾,当群辉进入SafeMode并通知其他服务器Shutdown时,我们就开始WaitServersDisconnect。

fsd)
    SynoUpsStateLog "synoups fsd"
    /usr/sbin/upsmon -c fsd
    UPSSafeMode $1
    WaitServersDisconnect 40 $2
    ;;

这里我设置循环40次,则最长等待时间为 40* 5s = 200s。

然后我们需要将shutdownups分支中直接进行的UPSShutdown操作修改为我们的WaitServersDisconnect,为了防止群辉直接ShutDownUPS。

shutdownups)
    SynoUpsStateLog "synoups shutdownups"
    WaitServersDisconnect 10 $2
    ;;

最后lowbatt分支也修改一下:

lowbatt)
    SynoUpsStateLog "synoups lowbatt"
    WaitServersDisconnect 10 $2
    ;;

到这里,魔改就结束了。

到这里终于把群辉上的UPS配置好了,我们下面开始进行ESXi设置。

设置ESXi

Step 1 下载ESXi社区UPS支持软件包

我这里是ESXi7,需要自己安装UPS支持软件包,我从谷歌上查到的应该都是使用的一个叫nut-tools(Network UPS Tools)的社区软件包,这里就直接贴作者博客上最新的下载地址了:

软件包下载地址

Step 2 ESXi开启SSH

首先进入ESXi Web UI:

然后通过 “管理=>服务” 进入服务管理页面,然后开启TSM-SSH服务:

Step 3 Esxi安装nut-client软件包

这里我们先SSH登入ESXi中

然后创建一个文件夹,我这里暂时使用tmp文件夹,然后将下载的nut-client包上传并解压到这个文件夹下(这里我是使用Xftp进行了上传):

[root@gaea:/tmp] tar -zxvf NutClient-ESXi-2.7.4-2.2.2.i386.tar.gz
readme.txt
upsmon-install.sh
upsmon-remove.sh
upsmon-update.sh
upsmon-2.7.4-2.2.2.i386.vib

然后需要执行 upsmon-install.sh文件:

chmod +x upsmon-install.sh
./upsmon-install.sh

这里一般都会挑一个提示,提示我们当前的软件接受级别较低无法安装社区级别的安装包,这是我们按照提示的指令将软件接收级别调到社区级别就可以了:

[root@gaea:/tmp] esxcli software acceptance set --level=CommunitySupported
Host acceptance level changed to 'CommunitySupported'.

然后再次安装:

[root@gaea:/tmp] ./upsmon-install.sh
Installation Result
   Message: Operation finished successfully.
   Reboot Required: false
   VIBs Installed: Margar_bootbank_upsmon_2.7.4-2.2.2
   VIBs Removed: 
   VIBs Skipped:

Step 4 ESXi设置nut-client

这时我们切换回ESXi Web UI,打开“管理 => 系统 => 高级设置”,然后在右上角的搜索中键入“nut”:

这里有三个地方需要修改:

  • UserVars.NutUser 这个应该都没有修改,直接填群辉upsd.users文件内默认的monuser就可以了
  • UserVars.NutUpsName 这里就直接写默认的 ups@“你的群辉内网IP”就可以了
  • UserVars.NutPasword 这个是我们在ups.users 文件中修改的password的值

Step 5 启动nut服务

在ESXi服务页面找到NutClient,然后启动该服务,并且设置启动策略为随主机启动和停止

修改好这三个地方后设置就完成了,下面我们需要测试一下:

Step 5 ESXi 测试

回到ESXi ssh,然后执行 /opt/nut/bin/upsc + “你在UserVars.NutUpsName设置的值”,如果出现下面这些UPS参数则说明设置成功,如果没有出现,检查一下你的群辉开启UPS服务器没,或者群辉和ESXi是否在同一个网段内,又或者上面的password设置有错误。

[root@tartarus:/tmp] /opt/nut/bin/upsc ups@xx.xx.xx.xx
battery.charge: 100
battery.voltage: 13.50
battery.voltage.high: 13.00
battery.voltage.low: 10.40
battery.voltage.nominal: 12.0
device.type: ups
driver.name: blazer_usb
driver.parameter.pollinterval: 5
driver.parameter.port: auto
driver.version: xxxxxxx
driver.version.internal: 0.11
input.current.nominal: 4.0
input.frequency: 50.0
input.frequency.nominal: 50
input.voltage: 243.5
input.voltage.fault: 243.5
input.voltage.nominal: 220
output.voltage: 200.0
ups.beeper.status: enabled
ups.delay.shutdown: 30
ups.delay.start: 180
ups.load: 34
ups.productid: 5161
ups.status: OL TRIM
ups.type: offline / line interactive
ups.vendorid: 0665

Step 6 群辉测试ESXi连接

通过upsc指令可以查看连接到UPS服务器上的客户端IP,这里可以检查ESXi是否连接到群辉上:

ash-4.3# upsc -c ups@localhost
192.168.106.21
192.168.106.22
127.0.0.1

断电测试

短时间断电测试

这里的短时间是指没有超过群辉上设置的进入安全模式之前的等待时间,即这样的操作不会触发服务器关机。

这里我直接把机房的总插座和群辉的网线拔了下来,然后马上再插上,制造一次人为的短时间断电断网(因为我暂时测试一下Link Down的情况),下面是群辉的邮件和DSM上的日志:

群辉服务器和NAS也向我的管理员邮箱中发出了邮件:

长时间断电测试

这里的长时间是指断电时间超过群辉上设置的”进入安全模式之前的等待时间”,即服务器会被通知关闭,同时群辉会在所有服务器关闭后将UPS关闭

长时间断电测试:

这里我就不贴ESXi上的AutoPowerOff事件了,但ESXi确实被触发了自动关机,同时群辉在等待ESXi关机后将UPS关机了。

总结

这一次设置UPS和修改synoups显著的提高了我对于Linux Shell的编程能力和对Linux PIPE理念的理解。耗时虽多,但是很值得。

随记

UPS测试备忘

我是用的UPS在通过群辉软件shutdown后实际只是切断了后部系统的电源,而UPS内部还开启着,因此如果在UPS被软件关闭后马上上电,则会发现后部系统没有电。这里需要等待1min左右让UPS完全关机,我这个UPS完全关机的特征是液晶屏幕熄灭无法触摸点亮。在UPS完全关机后再上电,这里后部系统就会通电而后设置了来电启动的都会被启动。

iDRAC Redfish

iDRAC Redfish API 白皮书

我之前一直无法进入DELL服务器的BIOS界面,我在写了一个通过Redfish API启动服务器的脚本之后才发现可以通过iDRAC带的VNC直接设置下次引导进入BIOS。因此这个调用RedfishAPI启动服务器的脚本其实是暂时没用的,但这里我还是贴到这里备忘:

function PowerOnR720() {
  return axios.post(
    "https://xxx.xxx.xxx.xxx/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset",
    { ResetType: "On" },
    {
      httpsAgent: new https.Agent({ rejectUnauthorized: false }),
      auth: { username: "xxxxxx", password: "xxxxxx" },
    }
  );
}

RedfishAPI使用的Auth方式是BasicAuthentication。同时在我这里无法通过http访问Redfish API,必须通过https访问。

同时Redfish API可以通过其RESTful API接口查看每一个API的描述和结构。

iDRAC 虚拟控制台

在iDRAC 概览=>服务器 栏右侧可以找到这个按钮:

点击会下载一个很短有效期的类似VNC的东西,打开后是这样的界面(需要JRE):

点击上面的下次引导菜单栏,可以选择下次引导打开BIOS,然后重启服务器就行了:

停电记录

2022-04-09停电一小时

停电后5mins内

今天傍晚我正在写LinuxSurface博客的时候突然停电了,我的屏幕瞬间熄灭,幸好Wordpres有定时自动保存的功能,在来电后数据没有掉。

在停电5s后我就听到了机房UPS的滴滴声,然后3mins左右机房所有设备都关机了,然后UPS被群辉切断了电源进入断电状态。

来电后15mins内

来电后,机房UPS接入AC电源,UPS发出长嘀声以示启动,然后后部设备电源接入,NAS、两个ESXi服务器都自动启动。ESXi服务器按顺序启动了我的各个服务,与外部网络通讯在大约在来电2mins后恢复,梯子大约在4mins时架上。15mins后所有Docker服务完全启动。但我的博客有显示ERROR DB CONNECTION问题,我猜测是由于宝塔面板的Mysql启动时间过长或异常导致,因此重启了宝塔面板的Mysql服务,然后再访问博客,博客就恢复了正常,后面写个脚本自动检测博客和Mysql服务状态并自动重启对应服务。

ESXi事件
群辉日志
开机启动邮件

由于我设置了群辉开机就刷新我的CDN地址,同时调用RedFish API尝试启动服务器(我其实已经设置了AC Recovery,但这个是保险)。这里贴一下邮件:

总体来说,这个UPS系统的目的达到了,可以让我不在家的时候可以自动恢复服务。


v1.0 wep 编写文章基本内容

v1.1 aep 添加停电记录

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注