Prometheus Exporter for CyberPower UPS


How to use strace for profit and winning

I’ve used a CyberPower UPS for quite a while to keep my important gear at home running in case of power disruptions. It’s been a great little unit, and hasn’t failed me yet.

Being consumer level equipment, it doesn’t support any of the useful protocols like SNMP for metrics gathering. Instead, you can connect it via USB to a computer and use their CLI utility to get some basic statistics.

I wanted this data in Prometheus so I can make some Grafana dashboards. To accomplish it, I wrote an exporter in Python. You can find the code here: https://gitlab.com/shouptech/cyberpower_exporter

Read below for more about how I wrote this.

The CLI

Using the CLI that comes with the cyberpower panel, you get the following:

# pwrstat -status

The UPS information shows as following:

	Properties:
		Model Name................... PR750LCD
		Firmware Number.............. PQ6BN2001641
		Rating Voltage............... 120 V
		Rating Power................. 525 Watt

	Current UPS status:
		State........................ Normal
		Power Supply by.............. Utility Power
		Utility Voltage.............. 122 V
		Output Voltage............... 122 V
		Battery Capacity............. 100 %
		Remaining Runtime............ 10 min.
		Load......................... 283 Watt(54 %)
		Line Interaction............. None
		Test Result.................. Passed at 2020/03/19 15:05:30
		Last Power Event............. None

These are great numbers and all, but I want to track them over time. So I wrote a prometheus exporter. This post details about how I used strace and python to get this data in a programatic way.

strace

strace is a neat utility that can be used to trace a programs execution. It will display all the syscalls an application makes, helping you figure out what the heck it does. Let’s run the status command again, but this time using strace:

# strace pwrstat -status
execve("/sbin/pwrstat", ["pwrstat", "-status"], 0x7fff907fd258 /* 30 vars */) = 0
brk(NULL)                               = 0x2247000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe81511e20) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=44237, ...}) = 0
mmap(NULL, 44237, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f41a4d30000
close(3)                                = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\2009\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=5993088, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f41a4d44000
mmap(NULL, 3942432, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f41a4750000
mprotect(0x7f41a4909000, 2097152, PROT_NONE) = 0
mmap(0x7f41a4b09000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b9000) = 0x7f41a4b09000
mmap(0x7f41a4b0f000, 14368, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f41a4b0f000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f41a4d45500) = 0
mprotect(0x7f41a4b09000, 16384, PROT_READ) = 0
mprotect(0x606000, 4096, PROT_READ)     = 0
mprotect(0x7f41a4d40000, 4096, PROT_READ) = 0
munmap(0x7f41a4d30000, 44237)           = 0
socket(AF_UNIX, SOCK_STREAM, 0)         = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/pwrstatd.ipc"}, 19) = 0
sendto(3, "STATUS\n\n", 8, 0, NULL, 0)  = 8
recvfrom(3, "STATUS\nstate=0\nmodel_name=PR750L"..., 512, 0, NULL, NULL) = 414
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
brk(NULL)                               = 0x2247000
brk(0x2268000)                          = 0x2268000
brk(NULL)                               = 0x2268000
write(1, "\n", 1
)                       = 1
write(1, "The UPS information shows as fol"..., 40The UPS information shows as following:
) = 40
write(1, "\n", 1
)                       = 1
write(1, "\tProperties:\n", 13	Properties:
)         = 13
write(1, "\t\tModel Name................... "..., 41		Model Name................... PR750LCD
) = 41
write(1, "\t\tFirmware Number.............. "..., 45		Firmware Number.............. PQ6BN2001641
) = 45
write(1, "\t\tRating Voltage............... "..., 38		Rating Voltage............... 120 V
) = 38
write(1, "\t\tRating Power................. "..., 41		Rating Power................. 525 Watt
) = 41
write(1, "\n", 1
)                       = 1
write(1, "\tCurrent UPS status:\n", 21	Current UPS status:
) = 21
write(1, "\t\tState........................ "..., 39		State........................ Normal
) = 39
write(1, "\t\tPower Supply by.............. "..., 46		Power Supply by.............. Utility Power
) = 46
write(1, "\t\tUtility Voltage.............. "..., 38		Utility Voltage.............. 122 V
) = 38
write(1, "\t\tOutput Voltage............... "..., 38		Output Voltage............... 122 V
) = 38
write(1, "\t\tBattery Capacity............. "..., 38		Battery Capacity............. 100 %
) = 38
write(1, "\t\tRemaining Runtime............ "..., 40		Remaining Runtime............ 10 min.
) = 40
write(1, "\t\tLoad......................... "..., 47		Load......................... 294 Watt(56 %)
) = 47
write(1, "\t\tLine Interaction............. "..., 37		Line Interaction............. None
) = 37
write(1, "\t\tTest Result.................. "..., 62		Test Result.................. Passed at 2020/03/19 15:05:30
) = 62
write(1, "\t\tLast Power Event............. "..., 37		Last Power Event............. None
) = 37
write(1, "\n", 1
)                       = 1
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

That certainly looks like a lot of info, and to be completely honest, I can’t tell you what most of it means. However, I can tell you how the application gets the data:

socket(AF_UNIX, SOCK_STREAM, 0)         = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/pwrstatd.ipc"}, 19) = 0
sendto(3, "STATUS\n\n", 8, 0, NULL, 0)  = 8
recvfrom(3, "STATUS\nstate=0\nmodel_name=PR750L"..., 512, 0, NULL, NULL) = 414

What do these lines do?

  1. Create a socket interface. We know we’re going to use a UNIX socket (AF_UNIX), and it will be a stream socket (SOCK_STREAM).
  2. Connect to the socket /var/pwrstatd.ipc.
  3. Send the data STATUS\n\n to the socket
  4. Receive some data from the socket. The data contains the current status of the UPS. By using the man page for recvfrom, we can also see that it receives 512 bytes from the socket.

Interacting in python

Let’s write a short python application that opens the socket, and prints out the received data. We can follow the same steps from the strace output to get the data into Python:

import socket

# Create a socket interface, family AF_UNIX, and type SOCK_STREAM
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# Connect the socket interface to /var/pwrstatd.ipc
s.connect('/var/pwrstatd.ipc')
# Send data to the socket
s.sendall(b'STATUS\n\n')
# Receive 512 bytes of data from the socket
data = s.recv(512)
# Decode the raw data as ascii, and print the resulting string
print(data.decode('ascii'))

Running this application, we get the following data:

# python pwrstat.py
STATUS
state=0
model_name=PR750LCD
firmware_num=PQ6BN2001641
battery_volt=24000
input_rating_volt=120000
output_rating_watt=525000
avr_supported=yes
online_type=no
diagnostic_result=1
diagnostic_date=2020/03/19 15:05:30
power_event_result=0
battery_remainingtime=621
battery_charging=no
battery_discharging=no
ac_present=yes
boost=no
buck=no
utility_volt=122000
output_volt=122000
load=50000
battery_capacity=100


From there, you can take the code, create some gauges using the prometheus_client package, and you get this code: https://gitlab.com/shouptech/cyberpower_exporter