mirror of
https://github.com/librenms/librenms-agent.git
synced 2024-05-09 09:54:52 +00:00
Add powermon app script (#348)
* added snmp/powermon-snmp.py * powermon script v1.3 * powermon script v1.3a * powermon-snmp.py v1.4
This commit is contained in:
362
snmp/powermon-snmp.py
Executable file
362
snmp/powermon-snmp.py
Executable file
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# Copyright(C) 2021 Ben Carbery yrebrac@upaya.net.au
|
||||
#
|
||||
# LICENSE - GPLv3
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# version 3. See https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
#
|
||||
# DESCRIPTION
|
||||
#
|
||||
# The script attempts to determine the current power consumption of the host via
|
||||
# one or more methods. The scripts should make it easier to add your own methods
|
||||
# if no included one is suitable for your host machine.
|
||||
#
|
||||
# The script should be called by the snmpd daemon on the host machine. This is
|
||||
# achieved via the 'extend' functionality in snmpd. For example, in
|
||||
# /etc/snmp/snmpd.conf:
|
||||
# extend powermon /usr/local/bin/powermon-snmp.py
|
||||
#
|
||||
# CUSTOMISING RESULTS
|
||||
#
|
||||
# The results can be accessed via the nsExtend MIBs from another host, e.g.
|
||||
# snmpwalk -v 2c -c <community_string> <host> \
|
||||
# <nsExtendConfigTable|nsExtendOutputFull|nsExtendOutput1Table>
|
||||
#
|
||||
# The results are returned in a JSON format suitable for graphing in LibreNMS.
|
||||
# A LibreNMS 'application' is available for this purpose.
|
||||
#
|
||||
# The application expects to see a single top-level reading in the results in
|
||||
# terms of Watts. This can be derived from a reading from one of the sub-
|
||||
# components, currently the ACPI 'meter' or 'psus'. But you must tell the script
|
||||
# which is the top-level or final reading you want to use in the results. This
|
||||
# allows you to sum results from dual PSUs or apply your own power factor for
|
||||
# example. To achieve this see the definition of 'data["reading"]' at the end
|
||||
# of the script, and modify as required. Two examples are provided.
|
||||
#
|
||||
# If you want to track your electricity cost you should also update the cost
|
||||
# per kWh value below. When you cost changes you can update the value. The
|
||||
# supply rate will be returned in the results
|
||||
#
|
||||
# COMPATIBILITY
|
||||
#
|
||||
# - Linux, not tested on other OS
|
||||
# - Tested on python 3.6, 3.8
|
||||
#
|
||||
# INSTALLATION
|
||||
#
|
||||
# - Sensors method: pip install PySensors
|
||||
# - hpasmcli method: install hp-health package for your distribution
|
||||
# - Copy this script somewhere, e.g. /usr/local/bin
|
||||
# - Uncomment costPerkWh and change the value
|
||||
# - Test then customise top-level reading
|
||||
# - Add the 'extend' config to snmpd.conf
|
||||
# - https://docs.librenms.org/Extensions/Applications/#powermon
|
||||
#
|
||||
# CHANGELOG
|
||||
#
|
||||
# 20210130 - v1.0 - initial, implemented PySensors method
|
||||
# 20210131 - v1.1 - implemented hpasmcli method
|
||||
# 20210204 - v1.2 - added top-level reading, librenms option
|
||||
# 20210205 - v1.3 - added cents per kWh
|
||||
# 20210205 - v1.4 - improvement to UI
|
||||
|
||||
version = 1.4
|
||||
|
||||
### Libraries
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getopt
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
### Option defaults
|
||||
|
||||
method = "" # must be one of methods array
|
||||
verbose = False
|
||||
warnings = False
|
||||
librenms = True # Return results in a JSON format suitable for Librenms
|
||||
# Set to false to return JSON data only
|
||||
pretty = False # Pretty printing
|
||||
|
||||
### Globals
|
||||
|
||||
error = 0
|
||||
errorString = ""
|
||||
data = {}
|
||||
result = {}
|
||||
usage = "USAGE: " + os.path.basename(__file__) + " [-h|--help] |" \
|
||||
+ " [-m|--method <method>] [-N|--no-librenms] [-p|--pretty]" \
|
||||
+ " [-v|--verbose] [-w|--warnings] | -l|--list-methods | -h|--help"
|
||||
methods = ["sensors", "hpasmcli"]
|
||||
#costPerkWh = 0.15 # <<<< UNCOMMENT
|
||||
|
||||
### General functions
|
||||
|
||||
def errorMsg(message):
|
||||
sys.stderr.write("ERROR: " + message + "\n")
|
||||
|
||||
def usageError(message="Invalid argument"):
|
||||
errorMsg(message)
|
||||
sys.stderr.write(usage + "\n")
|
||||
sys.exit(1)
|
||||
|
||||
def warningMsg(message):
|
||||
if verbose or warnings:
|
||||
sys.stderr.write("WARN: " + message + "\n")
|
||||
|
||||
def verboseMsg(message):
|
||||
if verbose:
|
||||
sys.stderr.write("INFO: " + message + "\n")
|
||||
|
||||
def listMethods():
|
||||
global verbose
|
||||
verbose = True
|
||||
verboseMsg("Available methods are: " + str(methods).strip('[]'))
|
||||
|
||||
### Data functions
|
||||
|
||||
def getData(method):
|
||||
if method == "sensors":
|
||||
data = getSensorData()
|
||||
|
||||
elif method == "hpasmcli":
|
||||
data = getHPASMData()
|
||||
else:
|
||||
usageError("You must specify a method.")
|
||||
|
||||
return data
|
||||
|
||||
def getSensorData():
|
||||
global error, errorString
|
||||
error = 2
|
||||
errorString = "No power sensor found"
|
||||
|
||||
try:
|
||||
import sensors
|
||||
sensors.init()
|
||||
|
||||
except ModuleNotFoundError as e:
|
||||
errorMsg(str(e))
|
||||
verboseMsg("Try 'pip install PySensors'")
|
||||
sys.exit(1)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
errorMsg("Module 'sensors' appears to be missing a dependancy: " + str(e))
|
||||
verboseMsg("Try 'dnf install lm_sensors'")
|
||||
sys.exit(1)
|
||||
|
||||
except:
|
||||
e = sys.exc_info()
|
||||
errorMsg("Module sensors is installed but failed to initialise: " + str(e))
|
||||
sys.exit(1)
|
||||
|
||||
sdata = {}
|
||||
sdata["meter"] = {}
|
||||
sdata["psu"] = {}
|
||||
|
||||
re_meter = "^power_meter"
|
||||
|
||||
power_chips = []
|
||||
try:
|
||||
for chip in sensors.iter_detected_chips():
|
||||
chip_name = str(chip)
|
||||
verboseMsg("Found chip: " + chip_name)
|
||||
|
||||
if re.search(re_meter, chip_name):
|
||||
verboseMsg("Found power meter: " + chip_name)
|
||||
error = 0
|
||||
errorString = ""
|
||||
|
||||
junk, meter_id = chip_name.split('acpi-', 1)
|
||||
sdata["meter"][meter_id] = {}
|
||||
|
||||
for feature in chip:
|
||||
feature_label = str(feature.label)
|
||||
verboseMsg("Found feature: " + feature_label)
|
||||
|
||||
if re.search("^power", feature_label):
|
||||
sdata["meter"][meter_id]["reading"] = feature.get_value()
|
||||
|
||||
if feature.get_value() == 0:
|
||||
# warning as downstream may try to divide by 0
|
||||
warningMsg("Sensors returned a zero value")
|
||||
|
||||
else:
|
||||
# store anything else in case label is something unexpected
|
||||
sdata[chip_name][feature_label] = feature.get_value()
|
||||
|
||||
except:
|
||||
es = sys.exc_info()
|
||||
error = 1
|
||||
errorString = "Unable to get data: General exception: " + str(es)
|
||||
|
||||
finally:
|
||||
sensors.cleanup()
|
||||
return sdata
|
||||
|
||||
def getHPASMData():
|
||||
global error, errorString
|
||||
|
||||
exe = shutil.which('hpasmcli')
|
||||
#if not os.access(candidate, os.W_OK):
|
||||
cmd = [exe, '-s', 'show powermeter; show powersupply']
|
||||
warningMsg("hpasmcli only runs as root")
|
||||
|
||||
try:
|
||||
output = subprocess.run(cmd, capture_output=True, check=True, text=True, timeout=2)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
errorMsg(str(e) + ": " + str(e.stdout).strip('\n'))
|
||||
sys.exit(1)
|
||||
|
||||
rawdata = str(output.stdout).replace('\t', ' ').replace('\n ', '\n').split('\n')
|
||||
|
||||
hdata = {}
|
||||
hdata["meter"] = {}
|
||||
hdata["psu"] = {}
|
||||
|
||||
re_meter = "^Power Meter #([0-9]+)"
|
||||
re_meter_reading = "^Power Reading :"
|
||||
re_psu = "^Power supply #[0-9]+"
|
||||
re_psu_present = "^Present :"
|
||||
re_psu_redundant = "^Redundant:"
|
||||
re_psu_condition = "^Condition:"
|
||||
re_psu_hotplug = "^Hotplug :"
|
||||
re_psu_reading = "^Power :"
|
||||
|
||||
for line in rawdata:
|
||||
if re.match(re_meter, line):
|
||||
verboseMsg("found power meter: " + line)
|
||||
junk, meter_id = line.split('#', 1)
|
||||
hdata["meter"][meter_id] = {}
|
||||
|
||||
elif re.match(re_meter_reading, line):
|
||||
verboseMsg("found power meter reading: " + line)
|
||||
junk, meter_reading = line.split(':', 1)
|
||||
hdata["meter"][meter_id]["reading"] = meter_reading.strip()
|
||||
|
||||
elif re.match(re_psu, line):
|
||||
verboseMsg("found power supply: " + line)
|
||||
junk, psu_id = line.split('#', 1)
|
||||
hdata["psu"][psu_id] = {}
|
||||
|
||||
elif re.match(re_psu_present, line):
|
||||
verboseMsg("found power supply present: " + line)
|
||||
junk, psu_present = line.split(':', 1)
|
||||
hdata["psu"][psu_id]["present"] = psu_present.strip()
|
||||
|
||||
elif re.match(re_psu_redundant, line):
|
||||
verboseMsg("found power supply redundant: " + line)
|
||||
junk, psu_redundant = line.split(':', 1)
|
||||
hdata["psu"][psu_id]["redundant"] = psu_redundant.strip()
|
||||
|
||||
elif re.match(re_psu_condition, line):
|
||||
verboseMsg("found power supply condition: " + line)
|
||||
junk, psu_condition = line.split(':', 1)
|
||||
hdata["psu"][psu_id]["condition"] = psu_condition.strip()
|
||||
|
||||
elif re.match(re_psu_hotplug, line):
|
||||
verboseMsg("found power supply hotplug: " + line)
|
||||
junk, psu_hotplug = line.split(':', 1)
|
||||
hdata["psu"][psu_id]["hotplug"] = psu_hotplug.strip()
|
||||
|
||||
elif re.match(re_psu_reading, line):
|
||||
verboseMsg("found power supply reading: " + line)
|
||||
junk, psu_reading = line.split(':', 1)
|
||||
hdata["psu"][psu_id]["reading"] = psu_reading.replace('Watts', '').strip()
|
||||
|
||||
return hdata
|
||||
|
||||
# Argument Parsing
|
||||
try:
|
||||
opts, args = getopt.gnu_getopt(
|
||||
sys.argv[1:], 'm:hlNpvw', ['method', 'help', 'list-methods', 'no-librenms', 'pretty', 'verbose', 'warnings']
|
||||
)
|
||||
if len(args) != 0:
|
||||
usageError("Unknown argument")
|
||||
|
||||
except getopt.GetoptError as e:
|
||||
usageError(str(e))
|
||||
|
||||
for opt, val in opts:
|
||||
if opt in ["-h", "--help"]:
|
||||
print(usage)
|
||||
sys.exit(0)
|
||||
|
||||
elif opt in ["-l", "--list-methods"]:
|
||||
listMethods()
|
||||
sys.exit(0)
|
||||
|
||||
elif opt in ["-m", "--method"]:
|
||||
if val not in methods:
|
||||
usageError("Invalid method: '" + val + "'")
|
||||
else:
|
||||
method = val
|
||||
|
||||
elif opt in ["-N", "--no-librenms"]:
|
||||
librenms = False
|
||||
|
||||
elif opt in ["-p", "--pretty"]:
|
||||
pretty = True
|
||||
|
||||
elif opt in ["-v", "--verbose"]:
|
||||
verbose = True
|
||||
|
||||
elif opt in ["-w", "--warnings"]:
|
||||
warnings = True
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
# Electricity Cost
|
||||
try:
|
||||
costPerkWh
|
||||
|
||||
except NameError:
|
||||
errorMsg("cost per kWh is undefined (uncomment in script)")
|
||||
sys.exit(1)
|
||||
|
||||
# Get data
|
||||
data = getData(method)
|
||||
data["supply"] = {}
|
||||
data["supply"]["rate"] = costPerkWh
|
||||
|
||||
# Top-level reading
|
||||
# CUSTOMISE THIS FOR YOUR HOST
|
||||
# i.e. by running with -p -n -m and see what you get and then updating where
|
||||
# in the JSON data the top-level reading is sourced from
|
||||
try:
|
||||
# Example 1 - take reading from ACPI meter id 1
|
||||
data["reading"] = data["meter"]["1"]["reading"]
|
||||
|
||||
# Example 2 - sum the two power supplies and apply a power factor
|
||||
#pf = 0.95
|
||||
#data["reading"] = str( float(data["psu"]["1"]["reading"]) \
|
||||
# + float(data["psu"]["2"]["reading"]) / pf )
|
||||
|
||||
except:
|
||||
data["reading"] = 0.0
|
||||
|
||||
# Build result
|
||||
if librenms:
|
||||
result['version']=version
|
||||
result['error']=error
|
||||
result['errorString']=errorString
|
||||
result['data']=data
|
||||
|
||||
else:
|
||||
result=data
|
||||
|
||||
# Print result
|
||||
if pretty:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
else:
|
||||
print(json.dumps(result))
|
||||
|
Reference in New Issue
Block a user