fix(inputs.upsd): Add additional fields to upsd from NUT (#14447)

This commit is contained in:
DaRK AnGeL 2024-01-05 17:46:26 +02:00 committed by GitHub
parent cbaca43e36
commit a1eb9f55c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 762 additions and 284 deletions

View File

@ -33,6 +33,19 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## consistently as floats to avoid database conflicts where some numbers are
## parsed as integers and others as floats.
# force_float = false
## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
## Wildcards are accepted.
# additional_fields = []
## Dump information for debugging
## Allows to print the raw variables (and corresponding types) as received
## from the NUT server ONCE for each UPS. The output is only available when
## running Telegraf in debug-mode.
## Please attach this information when reporting issues!
# dump_raw_variables = false
```
## Metrics

View File

@ -12,3 +12,16 @@
## consistently as floats to avoid database conflicts where some numbers are
## parsed as integers and others as floats.
# force_float = false
## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
## Wildcards are accepted.
# additional_fields = []
## Dump information for debugging
## Allows to print the raw variables (and corresponding types) as received
## from the NUT server ONCE for each UPS. The output is only available when
## running Telegraf in debug-mode.
## Please attach this information when reporting issues!
# dump_raw_variables = false

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_percent=100i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_voltage=24,firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_delay_shutdown=20i,ups_delay_start=30i,ups_status="OL"

View File

@ -0,0 +1 @@
[[inputs.upsd]]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_delay_shutdown=20i,ups_delay_start=30i,ups_status="OL" 1704213086754003102

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
additional_fields = ["battery.*"]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,device_mfr="CPS",device_type="ups",driver_debug=0i,driver_flag_allow_killpower=0i,driver_name="usbhid-ups",driver_parameter_pollfreq=30i,driver_parameter_pollinterval=2i,driver_parameter_port="auto",driver_parameter_product="CP900EPFCLCD",driver_parameter_productid=501i,driver_parameter_serial=0i,driver_parameter_synchronous="auto",driver_parameter_vendor="CPS",driver_parameter_vendorid=764i,driver_state="quiet",driver_version="2.8.1",driver_version_data="CyberPower HID 0.8",driver_version_internal=0.52,driver_version_usb="libusb-1.0.26 (API: 0x1000109)",firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_beeper_status=true,ups_delay_shutdown=20i,ups_delay_start=30i,ups_mfr="CPS",ups_model="CP900EPFCLCD",ups_productid=501i,ups_serial=0i,ups_status="OL",ups_test_result="No test initiated",ups_timer_shutdown=-60i,ups_timer_start=-60i,ups_vendorid=764i

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
additional_fields = ["*"]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700i,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL"

View File

@ -0,0 +1 @@
[[inputs.upsd]]

View File

@ -0,0 +1,15 @@
device.serial: STRING:64
device.model: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,15 @@
device.serial: ABC123
device.model: Model 12345
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL"

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
force_float = true

View File

@ -0,0 +1,15 @@
device.serial: STRING:64
device.model: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,15 @@
device.serial: ABC123
device.model: Model 12345
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL",device_location="Upper floor"

View File

@ -0,0 +1,3 @@
[[inputs.upsd]]
force_float = true
additional_fields = ["*"]

View File

@ -0,0 +1,16 @@
device.serial: STRING:64
device.model: STRING:64
device.location: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,16 @@
device.serial: ABC123
device.model: Model 12345
device.location: Upper floor
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -9,6 +9,7 @@ import (
nut "github.com/robbiet480/go.nut"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/plugins/inputs"
@ -17,37 +18,99 @@ import (
//go:embed sample.conf
var sampleConfig string
//See: https://networkupstools.org/docs/developer-guide.chunked/index.html
// see: https://networkupstools.org/docs/developer-guide.chunked/index.html
const defaultAddress = "127.0.0.1"
const defaultPort = 3493
type Upsd struct {
Server string `toml:"server"`
Port int `toml:"port"`
Username string `toml:"username"`
Password string `toml:"password"`
ForceFloat bool `toml:"force_float"`
// Define the set of variables _always_ included in a metric
var mandatoryVariableSet = map[string]bool{
"battery.date": true,
"battery.mfr.date": true,
"battery.runtime": true,
"device.model": true,
"device.serial": true,
"ups.firmware": true,
"ups.status": true,
}
Log telegraf.Logger `toml:"-"`
// Define the default field set to add if existing
var defaultFieldSet = map[string]string{
"battery.charge": "battery_charge_percent",
"battery.runtime.low": "battery_runtime_low",
"battery.voltage": "battery_voltage",
"input.frequency": "input_frequency",
"input.transfer.high": "input_transfer_high",
"input.transfer.low": "input_transfer_low",
"input.voltage": "input_voltage",
"ups.temperature": "internal_temp",
"ups.load": "load_percent",
"battery.voltage.nominal": "nominal_battery_voltage",
"input.voltage.nominal": "nominal_input_voltage",
"ups.realpower.nominal": "nominal_power",
"output.voltage": "output_voltage",
"ups.realpower": "real_power",
"ups.delay.shutdown": "ups_delay_shutdown",
"ups.delay.start": "ups_delay_start",
}
type Upsd struct {
Server string `toml:"server"`
Port int `toml:"port"`
Username string `toml:"username"`
Password string `toml:"password"`
ForceFloat bool `toml:"force_float"`
Additional []string `toml:"additional_fields"`
DumpRaw bool `toml:"dump_raw_variables"`
Log telegraf.Logger `toml:"-"`
filter filter.Filter
dumped map[string]bool
}
func (*Upsd) SampleConfig() string {
return sampleConfig
}
func (u *Upsd) Init() error {
// Compile the additional fields filter
f, err := filter.Compile(u.Additional)
if err != nil {
return fmt.Errorf("compiling additional_fields filter failed: %w", err)
}
u.filter = f
u.dumped = make(map[string]bool)
return nil
}
func (u *Upsd) Gather(acc telegraf.Accumulator) error {
upsList, err := u.fetchVariables(u.Server, u.Port)
if err != nil {
return err
}
if u.DumpRaw {
for name, variables := range upsList {
// Only dump the information once per UPS
if u.dumped[name] {
continue
}
values := make([]string, 0, len(variables))
types := make([]string, 0, len(variables))
for _, v := range variables {
values = append(values, fmt.Sprintf("%s: %v", v.Name, v.Value))
types = append(types, fmt.Sprintf("%s: %v", v.Name, v.OriginalType))
}
u.Log.Debugf("Variables dump for UPS %q:\n%s\n-----\n%s", name, strings.Join(values, "\n"), strings.Join(types, "\n"))
}
}
for name, variables := range upsList {
u.gatherUps(acc, name, variables)
}
return nil
}
func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.Variable) {
func (u *Upsd) gatherUps(acc telegraf.Accumulator, upsname string, variables []nut.Variable) {
metrics := make(map[string]interface{})
for _, variable := range variables {
name := variable.Name
@ -57,7 +120,7 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
tags := map[string]string{
"serial": fmt.Sprintf("%v", metrics["device.serial"]),
"ups_name": name,
"ups_name": upsname,
//"variables": variables.Status not sure if it's a good idea to provide this
"model": fmt.Sprintf("%v", metrics["device.model"]),
}
@ -75,54 +138,18 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
u.Log.Warnf("Converting 'battery.runtime' to 'time_left_ns' failed: %v", err)
}
// Add the mandatory information
fields := map[string]interface{}{
"battery_date": metrics["battery.date"],
"battery_mfr_date": metrics["battery.mfr.date"],
"status_flags": status,
"ups_status": metrics["ups.status"],
//Compatibility with apcupsd metrics format
// for compatibility with apcupsd metrics format
"time_left_ns": timeLeftNS,
}
floatValues := map[string]string{
"battery_charge_percent": "battery.charge",
"battery_runtime_low": "battery.runtime.low",
"battery_voltage": "battery.voltage",
"input_frequency": "input.frequency",
"input_transfer_high": "input.transfer.high",
"input_transfer_low": "input.transfer.low",
"input_voltage": "input.voltage",
"internal_temp": "ups.temperature",
"load_percent": "ups.load",
"nominal_battery_voltage": "battery.voltage.nominal",
"nominal_input_voltage": "input.voltage.nominal",
"nominal_power": "ups.realpower.nominal",
"output_voltage": "output.voltage",
"real_power": "ups.realpower",
"ups_delay_shutdown": "ups.delay.shutdown",
"ups_delay_start": "ups.delay.start",
}
for key, rawValue := range floatValues {
if metrics[rawValue] == nil {
continue
}
if !u.ForceFloat {
fields[key] = metrics[rawValue]
continue
}
// Force expected float values to actually being float (e.g. if delivered as int)
float, err := internal.ToFloat64(metrics[rawValue])
if err != nil {
acc.AddError(fmt.Errorf("converting %s=%v failed: %w", rawValue, metrics[rawValue], err))
continue
}
fields[key] = float
}
// Define the set of mandatory string fields
val, err := internal.ToString(metrics["ups.firmware"])
if err != nil {
acc.AddError(fmt.Errorf("converting ups.firmware=%q failed: %w", metrics["ups.firmware"], err))
@ -130,6 +157,36 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
fields["firmware"] = val
}
// Try to gather all default fields and optional field
for varname, v := range metrics {
// Skip all empty fields and all fields contained in the mandatory set
// of fields added above.
if v == nil || mandatoryVariableSet[varname] {
continue
}
// Use the name of the default field-set if present and otherwise check
// the additional field-set. If none of them contains the variable, we
// skip over it
var key string
if k, found := defaultFieldSet[varname]; found {
key = k
} else if u.filter != nil && u.filter.Match(varname) {
key = strings.ReplaceAll(varname, ".", "_")
} else {
continue
}
// Force expected float values to actually being float (e.g. if delivered as int)
if u.ForceFloat {
float, err := internal.ToFloat64(v)
if err == nil {
v = float
}
}
fields[key] = v
}
acc.AddFields("upsd", fields, tags)
}

View File

@ -1,159 +1,192 @@
package upsd
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
func TestUpsdGather(t *testing.T) {
nut := &Upsd{}
func TestBadServer(t *testing.T) {
// Create and start a server without interactions
server := &mockServer{}
ctx, cancel := context.WithCancel(context.Background())
addr, err := server.listen(ctx)
require.NoError(t, err)
defer cancel()
var (
tests = []struct {
name string
forceFloat bool
err bool
tags map[string]string
fields map[string]interface{}
out func() []interaction
}{
{
name: "test listening server with output",
forceFloat: false,
err: false,
tags: map[string]string{
"serial": "ABC123",
"ups_name": "fake",
"model": "Model 12345",
"status_OL": "true",
},
fields: map[string]interface{}{
"battery_charge_percent": float64(100),
"battery_date": nil,
"battery_mfr_date": "2016-07-26",
"battery_voltage": float64(13.4),
"firmware": "CUSTOM_FIRMWARE",
"input_voltage": float64(242),
"load_percent": float64(23),
"nominal_battery_voltage": float64(24),
"nominal_input_voltage": float64(230),
"nominal_power": int64(700),
"output_voltage": float64(230),
"real_power": float64(41),
"status_flags": uint64(8),
"time_left_ns": int64(600000000000),
"ups_status": "OL",
},
out: genOutput,
},
{
name: "test listening server with output & force floats",
forceFloat: true,
err: false,
tags: map[string]string{
"serial": "ABC123",
"ups_name": "fake",
"model": "Model 12345",
"status_OL": "true",
},
fields: map[string]interface{}{
"battery_charge_percent": float64(100),
"battery_date": nil,
"battery_mfr_date": "2016-07-26",
"battery_voltage": float64(13.4),
"firmware": "CUSTOM_FIRMWARE",
"input_voltage": float64(242),
"load_percent": float64(23),
"nominal_battery_voltage": float64(24),
"nominal_input_voltage": float64(230),
"nominal_power": int64(700),
"output_voltage": float64(230),
"real_power": float64(41),
"status_flags": uint64(8),
"time_left_ns": int64(600000000000),
"ups_status": "OL",
},
out: genOutput,
},
// Setup the plugin
plugin := &Upsd{
Server: addr.IP.String(),
Port: addr.Port,
}
require.NoError(t, plugin.Init())
// Do the query
var acc testutil.Accumulator
require.Error(t, plugin.Gather(&acc))
}
func TestCases(t *testing.T) {
// Get all directories in testdata
folders, err := os.ReadDir("testcases")
require.NoError(t, err)
// Register the plugin
inputs.Add("upsd", func() telegraf.Input {
return &Upsd{}
})
for _, f := range folders {
// Only handle folders
if !f.IsDir() {
continue
}
testcasePath := filepath.Join("testcases", f.Name())
configFilename := filepath.Join(testcasePath, "telegraf.conf")
expectedFilename := filepath.Join(testcasePath, "expected.out")
acc testutil.Accumulator
)
t.Run(f.Name(), func(t *testing.T) {
// Prepare the influx parser for expectations
parser := &influx.Parser{}
require.NoError(t, parser.Init())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
// Read the expected output if any
var expected []telegraf.Metric
if _, err := os.Stat(expectedFilename); err == nil {
var err error
expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser)
require.NoError(t, err)
}
lAddr, err := listen(ctx, t, tt.out())
// Setup a server from the input data
server, err := setupServer(testcasePath)
require.NoError(t, err)
nut.Server = (lAddr.(*net.TCPAddr)).IP.String()
nut.Port = (lAddr.(*net.TCPAddr)).Port
nut.ForceFloat = tt.forceFloat
// Start the server
ctx, cancel := context.WithCancel(context.Background())
addr, err := server.listen(ctx)
require.NoError(t, err)
defer cancel()
err = nut.Gather(&acc)
if tt.err {
require.Error(t, err)
} else {
require.NoError(t, err)
acc.AssertContainsFields(t, "upsd", tt.fields)
acc.AssertContainsTaggedFields(t, "upsd", tt.fields, tt.tags)
}
cancel()
// Configure the plugin
cfg := config.NewConfig()
require.NoError(t, cfg.LoadConfig(configFilename))
require.Len(t, cfg.Inputs, 1)
plugin := cfg.Inputs[0].Input.(*Upsd)
plugin.Server = addr.IP.String()
plugin.Port = addr.Port
require.NoError(t, plugin.Init())
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
// Check the metric nevertheless as we might get some metrics despite errors.
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
acc.Lock()
defer acc.Unlock()
require.Empty(t, acc.Errors)
})
}
}
func TestUpsdGatherFail(t *testing.T) {
nut := &Upsd{}
type interaction struct {
Expected string
Response string
}
var (
tests = []struct {
name string
err bool
tags map[string]string
fields map[string]interface{}
out func() []interaction
}{
{
name: "test with bad output",
err: true,
out: genBadOutput,
},
}
type variable struct {
Name string
Value string
}
acc testutil.Accumulator
)
type mockServer struct {
protocol []interaction
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
lAddr, err := listen(ctx, t, tt.out())
require.NoError(t, err)
nut.Server = (lAddr.(*net.TCPAddr)).IP.String()
nut.Port = (lAddr.(*net.TCPAddr)).Port
err = nut.Gather(&acc)
if tt.err {
require.Error(t, err)
} else {
require.NoError(t, err)
acc.AssertContainsTaggedFields(t, "upsd", tt.fields, tt.tags)
}
cancel()
})
func (s *mockServer) init() {
s.protocol = []interaction{
{
Expected: "VER\n",
Response: "1\n",
},
{
Expected: "NETVER\n",
Response: "1\n",
},
{
Expected: "LIST UPS\n",
Response: "BEGIN LIST UPS\nUPS fake \"fake UPS\"\nEND LIST UPS\n",
},
{
Expected: "LIST CLIENT fake\n",
Response: "BEGIN LIST CLIENT fake\nCLIENT fake 127.0.0.1\nEND LIST CLIENT fake\n",
},
{
Expected: "LIST CMD fake\n",
Response: "BEGIN LIST CMD fake\nEND LIST CMD fake\n",
},
{
Expected: "GET UPSDESC fake\n",
Response: "UPSDESC fake \"stub-ups-description\"\n",
},
{
Expected: "GET NUMLOGINS fake\n",
Response: "NUMLOGINS fake 1\n",
},
}
}
func listen(ctx context.Context, t *testing.T, out []interaction) (net.Addr, error) {
func (s *mockServer) addVariables(variables []variable, types map[string]string) error {
// Add a VAR entries for the variables
values := make([]string, 0, len(variables))
for _, v := range variables {
values = append(values, fmt.Sprintf("VAR fake %s %q", v.Name, v.Value))
}
s.protocol = append(s.protocol, interaction{
Expected: "LIST VAR fake\n",
Response: "BEGIN LIST VAR fake\n" + strings.Join(values, "\n") + "\nEND LIST VAR fake\n",
})
// Add a description and type interaction for the variable
for _, v := range variables {
variableType, found := types[v.Name]
if !found {
return fmt.Errorf("type for variable %q not found", v.Name)
}
s.protocol = append(s.protocol,
interaction{
Expected: "GET DESC fake " + v.Name + "\n",
Response: "DESC fake" + v.Name + " \"No description here\"\n",
},
interaction{
Expected: "GET TYPE fake " + v.Name + "\n",
Response: "TYPE fake " + v.Name + " " + variableType + "\n",
},
)
}
return nil
}
func (s *mockServer) listen(ctx context.Context) (*net.TCPAddr, error) {
lc := net.ListenConfig{}
ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:0")
if err != nil {
@ -170,131 +203,90 @@ func listen(ctx context.Context, t *testing.T, out []interaction) (net.Addr, err
return
}
defer conn.Close()
require.NoError(t, conn.SetReadDeadline(time.Now().Add(time.Minute)))
_ = conn.SetReadDeadline(time.Now().Add(time.Minute))
in := make([]byte, 128)
for _, interaction := range out {
for _, interaction := range s.protocol {
n, err := conn.Read(in)
require.NoError(t, err, "failed to read from connection")
if err != nil {
fmt.Printf("Failed to read from connection: %v\n", err)
return
}
expectedBytes := []byte(interaction.Expected)
want, got := expectedBytes, in[:n]
require.Equal(t, want, got)
request := in[:n]
if !bytes.Equal([]byte(interaction.Expected), request) {
fmt.Printf("Unexpected request %q, expected %q\n", string(request), interaction.Expected)
return
}
_, err = conn.Write([]byte(interaction.Response))
require.NoError(t, err, "failed to respond to LIST UPS")
if _, err := conn.Write([]byte(interaction.Response)); err != nil {
fmt.Printf("Cannot write answer for request %q: %v\n", string(request), err)
return
}
}
// Append EOF to end of output bytes
_, err = conn.Write([]byte{0, 0})
require.NoError(t, err, "failed to write EOF")
if _, err := conn.Write([]byte{0, 0}); err != nil {
fmt.Printf("Cannot write EOF: %v\n", err)
return
}
}()
}
}()
return ln.Addr(), nil
return ln.Addr().(*net.TCPAddr), nil
}
type interaction struct {
Expected string
Response string
}
func setupServer(path string) (*mockServer, error) {
// Read the variables
varbuf, err := os.ReadFile(filepath.Join(path, "variables.dev"))
if err != nil {
return nil, fmt.Errorf("reading variables failed: %w", err)
}
func genOutput() []interaction {
m := make([]interaction, 0)
m = append(m,
interaction{
Expected: "VER\n",
Response: "1\n",
},
interaction{
Expected: "NETVER\n",
Response: "1\n",
},
interaction{
Expected: "LIST UPS\n",
Response: `BEGIN LIST UPS
UPS fake "fakescription"
END LIST UPS
`,
},
interaction{
Expected: "LIST CLIENT fake\n",
Response: `BEGIN LIST CLIENT fake
CLIENT fake 192.168.1.1
END LIST CLIENT fake
`,
},
interaction{
Expected: "LIST CMD fake\n",
Response: `BEGIN LIST CMD fake
END LIST CMD fake
`,
},
interaction{
Expected: "GET UPSDESC fake\n",
Response: "UPSDESC fake \"stub-ups-description\"\n",
},
interaction{
Expected: "GET NUMLOGINS fake\n",
Response: "NUMLOGINS fake 1\n",
},
interaction{
Expected: "LIST VAR fake\n",
Response: `BEGIN LIST VAR fake
VAR fake device.serial "ABC123"
VAR fake device.model "Model 12345"
VAR fake input.voltage "242.0"
VAR fake ups.load "23.0"
VAR fake battery.charge "100.0"
VAR fake battery.runtime "600.00"
VAR fake output.voltage "230.0"
VAR fake battery.voltage "13.4"
VAR fake input.voltage.nominal "230.0"
VAR fake battery.voltage.nominal "24.0"
VAR fake ups.realpower "41.0"
VAR fake ups.realpower.nominal "700"
VAR fake ups.firmware "CUSTOM_FIRMWARE"
VAR fake battery.mfr.date "2016-07-26"
VAR fake ups.status "OL"
END LIST VAR fake
`,
},
)
m = appendVariable(m, "device.serial", "STRING:64")
m = appendVariable(m, "device.model", "STRING:64")
m = appendVariable(m, "input.voltage", "NUMBER")
m = appendVariable(m, "ups.load", "NUMBER")
m = appendVariable(m, "battery.charge", "NUMBER")
m = appendVariable(m, "battery.runtime", "NUMBER")
m = appendVariable(m, "output.voltage", "NUMBER")
m = appendVariable(m, "battery.voltage", "NUMBER")
m = appendVariable(m, "input.voltage.nominal", "NUMBER")
m = appendVariable(m, "battery.voltage.nominal", "NUMBER")
m = appendVariable(m, "ups.realpower", "NUMBER")
m = appendVariable(m, "ups.realpower.nominal", "NUMBER")
m = appendVariable(m, "ups.firmware", "STRING:64")
m = appendVariable(m, "battery.mfr.date", "STRING:64")
m = appendVariable(m, "ups.status", "STRING:64")
// Parse the information into variable names and values (upsc format)
variables := make([]variable, 0)
scanner := bufio.NewScanner(bytes.NewBuffer(varbuf))
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("cannot parse line %s", line)
}
name := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
variables = append(variables, variable{name, value})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("processing variables failed: %w", err)
}
return m
}
// Read the variable-type mapping
typebuf, err := os.ReadFile(filepath.Join(path, "types.dev"))
if err != nil {
return nil, fmt.Errorf("reading variables failed: %w", err)
}
func appendVariable(m []interaction, name string, typ string) []interaction {
m = append(m,
interaction{
Expected: "GET DESC fake " + name + "\n",
Response: "DESC fake" + name + " \"No description here\"\n",
},
interaction{
Expected: "GET TYPE fake " + name + "\n",
Response: "TYPE fake " + name + " " + typ + "\n",
},
)
return m
}
// Parse the information into variable names and values (upsc format)
types := make(map[string]string, 0)
scanner = bufio.NewScanner(bytes.NewBuffer(typebuf))
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("cannot parse line %s", line)
}
name := strings.TrimSpace(parts[0])
vartype := strings.TrimSpace(parts[1])
types[name] = vartype
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("processing variables failed: %w", err)
}
func genBadOutput() []interaction {
m := make([]interaction, 0)
return m
// Setup the server and add the device information
server := &mockServer{}
server.init()
err = server.addVariables(variables, types)
return server, err
}