1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,111 @@
# UPSD Input Plugin
This plugin reads data of one or more Uninterruptible Power Supplies
from an `upsd` daemon using its NUT network protocol.
## Requirements
`upsd` should be installed and it's daemon should be running.
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Monitor UPSes connected via Network UPS Tools
[[inputs.upsd]]
## A running NUT server to connect to.
## IPv6 addresses must be enclosed in brackets (e.g. "[::1]")
# server = "127.0.0.1"
# port = 3493
# username = "user"
# password = "password"
## Force parsing numbers as floats
## It is highly recommended to enable this setting to parse numbers
## 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.
## Please attach this information when reporting issues!
# log_level = "trace"
```
## Pitfalls
Please note that field types are automatically determined based on the values.
Especially the strings `enabled` and `disabled` are automatically converted to
`boolean` values. This might lead to trouble for fields that can contain
non-binary values like `enabled`, `disabled` and `muted` as the output field
will be `boolean` for the first two values but `string` for the latter. To
convert `enabled` and `disabled` values back to `string` for those fields, use
the [enum processor][enum_processor] with
```toml
[[processors.enum]]
[[processors.enum.mapping]]
field = "ups_beeper_status"
[processors.enum.mapping.value_mappings]
true = "enabled"
false = "disabled"
```
Alternatively, you can also map the non-binary value to a `boolean`.
[enum_processor]: ../../processors/enum/README.md
## Metrics
This implementation tries to maintain compatibility with the apcupsd metrics:
- upsd
- tags:
- serial
- ups_name
- model
- fields:
- status_flags ([status-bits][rfc9271-sec5.1])
- input_voltage
- load_percent
- battery_charge_percent
- time_left_ns
- output_voltage
- internal_temp
- battery_voltage
- input_frequency
- battery_date
- nominal_input_voltage
- nominal_battery_voltage
- nominal_power
- firmware
With the exception of:
- tags:
- status (string representing the set status_flags)
- fields:
- time_on_battery_ns
[rfc9271-sec5.1]: https://www.rfc-editor.org/rfc/rfc9271.html#section-5.1
## Example Output
```text
upsd,serial=AS1231515,ups_name=name1 load_percent=9.7,time_left_ns=9800000,output_voltage=230.4,internal_temp=32.4,battery_voltage=27.4,input_frequency=50.2,input_voltage=230.4,battery_charge_percent=100,status_flags=8i 1490035922000000000
```

View file

@ -0,0 +1,26 @@
# Monitor UPSes connected via Network UPS Tools
[[inputs.upsd]]
## A running NUT server to connect to.
## IPv6 addresses must be enclosed in brackets (e.g. "[::1]")
# server = "127.0.0.1"
# port = 3493
# username = "user"
# password = "password"
## Force parsing numbers as floats
## It is highly recommended to enable this setting to parse numbers
## 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.
## Please attach this information when reporting issues!
# log_level = "trace"

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

284
plugins/inputs/upsd/upsd.go Normal file
View file

@ -0,0 +1,284 @@
//go:generate ../../../tools/readme_config_includer/generator
package upsd
import (
_ "embed"
"fmt"
"strings"
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"
)
//go:embed sample.conf
var sampleConfig string
var (
// Define the set of variables _always_ included in a metric
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,
}
// Define the default field set to add if existing
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",
}
)
const (
defaultAddress = "127.0.0.1"
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"`
Additional []string `toml:"additional_fields"`
DumpRaw bool `toml:"dump_raw_variables" deprecated:"1.35.0;use 'log_level' 'trace' instead"`
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.Log.Level().Includes(telegraf.Trace) || u.DumpRaw { // for backward compatibility
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.Tracef("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, upsname string, variables []nut.Variable) {
metrics := make(map[string]interface{})
for _, variable := range variables {
name := variable.Name
value := variable.Value
metrics[name] = value
}
tags := map[string]string{
"serial": fmt.Sprintf("%v", metrics["device.serial"]),
"ups_name": upsname,
// "variables": variables.Status not sure if it's a good idea to provide this
"model": fmt.Sprintf("%v", metrics["device.model"]),
}
// For compatibility with the apcupsd plugin's output we map the status string status into a bit-format
status := mapStatus(metrics, tags)
timeLeftS, err := internal.ToFloat64(metrics["battery.runtime"])
if err != nil {
u.Log.Warnf("Type for 'battery.runtime' is not supported: %v", err)
}
timeLeftNS, err := internal.ToInt64(timeLeftS * 1_000_000_000)
if err != nil {
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"],
// for compatibility with apcupsd metrics format
"time_left_ns": timeLeftNS,
}
// 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))
} else {
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)
}
func mapStatus(metrics map[string]interface{}, tags map[string]string) uint64 {
status := uint64(0)
statusString := fmt.Sprintf("%v", metrics["ups.status"])
statuses := strings.Fields(statusString)
// Source: 1.3.2 at http://rogerprice.org/NUT/ConfigExamples.A5.pdf
// apcupsd bits:
// 0 Runtime calibration occurring (Not reported by Smart UPS v/s and BackUPS Pro)
// 1 SmartTrim (Not reported by 1st and 2nd generation SmartUPS models)
// 2 SmartBoost
// 3 On line (this is the normal condition)
// 4 On battery
// 5 Overloaded output
// 6 Battery low
// 7 Replace battery
if choice.Contains("CAL", statuses) {
status |= 1 << 0
tags["status_CAL"] = "true"
}
if choice.Contains("TRIM", statuses) {
status |= 1 << 1
tags["status_TRIM"] = "true"
}
if choice.Contains("BOOST", statuses) {
status |= 1 << 2
tags["status_BOOST"] = "true"
}
if choice.Contains("OL", statuses) {
status |= 1 << 3
tags["status_OL"] = "true"
}
if choice.Contains("OB", statuses) {
status |= 1 << 4
tags["status_OB"] = "true"
}
if choice.Contains("OVER", statuses) {
status |= 1 << 5
tags["status_OVER"] = "true"
}
if choice.Contains("LB", statuses) {
status |= 1 << 6
tags["status_LB"] = "true"
}
if choice.Contains("RB", statuses) {
status |= 1 << 7
tags["status_RB"] = "true"
}
return status
}
func (u *Upsd) fetchVariables(server string, port int) (map[string][]nut.Variable, error) {
client, err := nut.Connect(server, port)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
if u.Username != "" && u.Password != "" {
_, err = client.Authenticate(u.Username, u.Password)
if err != nil {
return nil, fmt.Errorf("auth: %w", err)
}
}
upsList, err := client.GetUPSList()
if err != nil {
return nil, fmt.Errorf("getupslist: %w", err)
}
defer func() {
_, disconnectErr := client.Disconnect()
if disconnectErr != nil {
err = fmt.Errorf("disconnect: %w", disconnectErr)
}
}()
result := make(map[string][]nut.Variable)
for _, ups := range upsList {
result[ups.Name] = ups.Variables
}
return result, err
}
func init() {
inputs.Add("upsd", func() telegraf.Input {
return &Upsd{
Server: defaultAddress,
Port: defaultPort,
}
})
}

View file

@ -0,0 +1,295 @@
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 TestBadServer(t *testing.T) {
// Create and start a server without interactions
server := &mockServer{}
ctx, cancel := context.WithCancel(t.Context())
addr, err := server.listen(ctx)
require.NoError(t, err)
defer cancel()
// 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")
t.Run(f.Name(), func(t *testing.T) {
// Prepare the influx parser for expectations
parser := &influx.Parser{}
require.NoError(t, parser.Init())
// 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)
}
// Setup a server from the input data
server, err := setupServer(testcasePath)
require.NoError(t, err)
// Start the server
ctx, cancel := context.WithCancel(t.Context())
addr, err := server.listen(ctx)
require.NoError(t, err)
defer 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)
})
}
}
type interaction struct {
expected string
response string
}
type variable struct {
name string
value string
}
type mockServer struct {
protocol []interaction
}
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 (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 {
return nil, err
}
go func() {
defer ln.Close()
for ctx.Err() == nil {
func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
err = conn.SetReadDeadline(time.Now().Add(time.Minute))
if err != nil {
return
}
in := make([]byte, 128)
for _, interaction := range s.protocol {
n, err := conn.Read(in)
if err != nil {
fmt.Printf("Failed to read from connection: %v\n", err)
return
}
request := in[:n]
if !bytes.Equal([]byte(interaction.expected), request) {
fmt.Printf("Unexpected request %q, expected %q\n", string(request), interaction.expected)
return
}
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
if _, err := conn.Write([]byte{0, 0}); err != nil {
fmt.Printf("Cannot write EOF: %v\n", err)
return
}
}()
}
}()
return ln.Addr().(*net.TCPAddr), nil
}
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)
}
// 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)
}
// 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)
}
// 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)
}
// Setup the server and add the device information
server := &mockServer{}
server.init()
err = server.addVariables(variables, types)
return server, err
}