Archives for APC UPS

Graceful Shutdown with APC BackUPS HS 500

APC’s BackUPS HS 500 UPS has been around forever, as has it’s firmware. Still, a wall-mountable network connected UPS with three switchable outputs for less than £100 from one of the most reputable brands seems like a good deal to me.

backups-hs-500-banner

There are issues with this product though. The web interface doesn’t work on any recent browser and it’s also impossible to configure it without Windows 2000 (XP if you’re lucky). So graceful shutdown is a non-starter then.

Having suffered a few prolonged power-outages recently I thought perhaps it’s time this problem was solved. Fortunately, Anton Bagayev has posted on Github a script to control the outlets from Linux, using Curl to interact with it’s primitive web interface, and this can easily be adapted into a shutdown script – I’ve called this check-power and have it running via cron every minute. When the UPS is on battery and run-time goes to 13 minutes or less, it calls some other shutdown script as needed:

#!/bin/bash
# This script uses the web interface of the APC BackUPS HS-500 to check it's status, and
# calls some other script to effect a host shutdown, should the UPS be on battery and the
# runtime be less than 13 minutes.
# temp file for operational status - battery level etc
STATUS="/tmp/apc-500-status.tmp"
UPS="[ip-address-goes-here]"

# get output values from web-control
curl -sl "http://$UPS/status.cgi" | tr -dc '[:print:]\n' > $STATUS
# Extract the unit operating status fields - battery level etc
LOAD="$(cat $STATUS | grep -o '[0-9]* Watts' | grep -o '[0-9]*')"
BATTERYLEVEL="$(cat $STATUS | grep -o '[0-9]* %' | grep -o '[0-9]*')"
RUNTIME="$(cat $STATUS | grep -o '[0-9]* minutes' | grep -o '[0-9]*')"
BATTERYSTATUS="$(cat $STATUS | egrep -o 'Charged|Charging|Discharged|Discharging')"
UPSSTATUS="$(cat $STATUS | egrep -o 'On Line|On Battery' | sed 's/ / /g')"
LASTTEST="$(cat $STATUS | egrep -o 'Result of last self-test is:.*(Passed|Failed)</font>' | egrep -o '(Passed|Failed)')"
LASTTRANSFER="$(cat $STATUS | egrep -o 'No&nbsp;Transfer|Blackout' | sed 's/&nbsp;/ /g')"
# show active configuration
logger "UPS Status $UPSSTATUS, $RUNTIME minutes remaining (load: $LOAD Watts)"
if [ "$UPSSTATUS" == "On Battery" ]; then
 if [ $RUNTIME -le 13 ]; then
 logger "UPS Critical: $RUNTIME minutes remaining. Starting shutdown procedure."
 [call-shutdown-script-goes-here]
 fi
fi
# garbage collector
rm -f $STATUS

OK so now we can control it with Anton’s script – the three outputs are individually switchable – and monitor it with this script, but what about configuration? Firing up a Windows 2000 VM and grabbing some Wireshark captures from the supplied configuration utility (really APC?), this is pretty straightforward too. The utility interacts with the UPS via UDP broadcast with some special command codes to make it do things like set the IP.

With a bit of fiddling, this too can be scripted Linux with a few dependencies (arping, xxd, socat). I’ve called this apc500.sh, and it can set the IP address and name of the device from the Linux command line, and uses Anton’s apc.sh to show device status (which can be modified per the above script to add battery levels etc if required):

#!/bin/bash
# UPS Management script for APC 500 HS
#
# -f - to Find and show detail of the device
# -s - to Set the IP address of the device (0.0.0.0 for DHCP)
# -n - to set the Name of the device
# Command line parameters - what are we doing?
for i in "$@"
do
case $i in
 -f*|--find*)
 FUNCTION="FIND"
 ;;
 -s=*|--setip=*)
 FUNCTION="SETIP"
 IPADDRESS="${i#*=}"
 shift # past argument=value
 ;;
 -n=*|--setname=*)
 FUNCTION="SETNAME"
 NAME="${i#*=}"
 shift # past argument=value
 ;;
 *)
 # unknown option
 ;;
esac
done
if [ "$FUNCTION" = "FIND" ]; then
 # Find UPS via broadcast
 DATA="$(echo '11 50 00 A0 10 50 43 43' | xxd -r -p | socat - UDP4-DATAGRAM:255.255.255.255:9950,so-broadcast,sourceport=9951 | tr -dc '[:print:]\n')"
 MODEL=${DATA:9:15}
 SERIAL=${DATA:24:12}
 MACADD=${DATA:37:12}
 TMP=${MACADD,,}
 MACADDR=${TMP:0:2}:${TMP:2:2}:${TMP:4:2}:${TMP:6:2}:${TMP:8:2}:${TMP:10:2}
 TMP=${DATA:52}
 NAME=${TMP::-1}
 # And lookup the MAC from ARP cache
 IPADD="$(arp -an | grep "$MACADDR" | egrep -o '[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*')"
 # Likely, there was nothing there. Check, and use arping (needs root) if we need to
 case "$IPADD" in
 "") IPADD="$(arping "$MACADDR" -c 2 -i eth0 | egrep -o -m 1 '[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*' | head -1)" ;;
 esac
# Print out the information
 echo Model: $MODEL
 echo Serial: $SERIAL
 echo Name: $NAME
 echo MAC Address: $MACADDR
# Now check again to see if we have an IP address. If we do, we can get run-time information, eg battery level etc
 case "$IPADD" in
 "")
 echo "IP Address: Not known"
 ;;
 *)
 echo "IP Address: $IPADD"
 ./apc.sh ip=$IPADD status
 esac
fi;
if [ "$FUNCTION" = "SETIP" ]; then
 # Work out IP in Hex
 IPDEC="$(echo "$IPADDRESS" | sed 's/\./ /g')"
 TMP="$(printf '%02x ' $IPDEC ; echo)"
 IPHEX=${TMP::-1}
 RESULT="$(echo '12 50 00 a0 98 05 45 43 f5 f4 34 f6 '"$IPHEX" | xxd -r -p | socat - UDP4-DATAGRAM:255.255.255.255:9950,so-broadcast | tr -dc '[:print:]\n')"
 case "$RESULT" in
 "") echo "UPS did not respond.";;
 *) echo "UPS acknowledged command."
 esac
fi;
if [ "$FUNCTION" = "SETNAME" ]; then
 TMP="$(echo "$NAME" | xxd -p )"
 NAMEHEX=${TMP::-2}
 RESULT="$(echo '12 50 00 a0 10 08 45 43 f5 '"$NAMEHEX"' 00' | xxd -r -p | socat - UDP4-DATAGRAM:255.255.255.255:9950,so-broadcast | tr -dc '[:print:]\n')"
 case "$RESULT" in
 "") echo "UPS did not respond.";;
 *) echo "UPS acknowledged command."
 esac
fi;
# End of script.