diff options
Diffstat (limited to '')
12 files changed, 1920 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/upsd/README.md b/src/go/collectors/go.d.plugin/modules/upsd/README.md new file mode 120000 index 000000000..8dcef84dd --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/README.md @@ -0,0 +1 @@ +integrations/ups_nut.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/upsd/charts.go b/src/go/collectors/go.d.plugin/modules/upsd/charts.go new file mode 100644 index 000000000..72bd69f4f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/charts.go @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +import ( + "fmt" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" +) + +const ( + prioUpsLoad = module.Priority + iota + prioUpsLoadUsage + prioUpsStatus + prioUpsTemperature + + prioBatteryCharge + prioBatteryEstimatedRuntime + prioBatteryVoltage + prioBatteryVoltageNominal + + prioInputVoltage + prioInputVoltageNominal + prioInputCurrent + prioInputCurrentNominal + prioInputFrequency + prioInputFrequencyNominal + + prioOutputVoltage + prioOutputVoltageNominal + prioOutputCurrent + prioOutputCurrentNominal + prioOutputFrequency + prioOutputFrequencyNominal +) + +var upsChartsTmpl = module.Charts{ + upsLoadChartTmpl.Copy(), + upsLoadUsageChartTmpl.Copy(), + upsStatusChartTmpl.Copy(), + upsTemperatureChartTmpl.Copy(), + + upsBatteryChargePercentChartTmpl.Copy(), + upsBatteryEstimatedRuntimeChartTmpl.Copy(), + upsBatteryVoltageChartTmpl.Copy(), + upsBatteryVoltageNominalChartTmpl.Copy(), + + upsInputVoltageChartTmpl.Copy(), + upsInputVoltageNominalChartTmpl.Copy(), + upsInputCurrentChartTmpl.Copy(), + upsInputCurrentNominalChartTmpl.Copy(), + upsInputFrequencyChartTmpl.Copy(), + upsInputFrequencyNominalChartTmpl.Copy(), + + upsOutputVoltageChartTmpl.Copy(), + upsOutputVoltageNominalChartTmpl.Copy(), + upsOutputCurrentChartTmpl.Copy(), + upsOutputCurrentNominalChartTmpl.Copy(), + upsOutputFrequencyChartTmpl.Copy(), + upsOutputFrequencyNominalChartTmpl.Copy(), +} + +var ( + upsLoadChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.load_percentage", + Title: "UPS load", + Units: "percentage", + Fam: "ups", + Ctx: "upsd.ups_load", + Priority: prioUpsLoad, + Type: module.Area, + Dims: module.Dims{ + {ID: "ups_%s_ups.load", Name: "load", Div: varPrecision}, + }, + } + upsLoadUsageChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.load_usage", + Title: "UPS load usage (power output)", + Units: "Watts", + Fam: "ups", + Ctx: "upsd.ups_load_usage", + Priority: prioUpsLoadUsage, + Dims: module.Dims{ + {ID: "ups_%s_ups.load.usage", Name: "load_usage", Div: varPrecision}, + }, + } + upsStatusChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.status", + Title: "UPS status", + Units: "status", + Fam: "ups", + Ctx: "upsd.ups_status", + Priority: prioUpsStatus, + Dims: module.Dims{ + {ID: "ups_%s_ups.status.OL", Name: "on_line"}, + {ID: "ups_%s_ups.status.OB", Name: "on_battery"}, + {ID: "ups_%s_ups.status.LB", Name: "low_battery"}, + {ID: "ups_%s_ups.status.HB", Name: "high_battery"}, + {ID: "ups_%s_ups.status.RB", Name: "replace_battery"}, + {ID: "ups_%s_ups.status.CHRG", Name: "charging"}, + {ID: "ups_%s_ups.status.DISCHRG", Name: "discharging"}, + {ID: "ups_%s_ups.status.BYPASS", Name: "bypass"}, + {ID: "ups_%s_ups.status.CAL", Name: "calibration"}, + {ID: "ups_%s_ups.status.OFF", Name: "offline"}, + {ID: "ups_%s_ups.status.OVER", Name: "overloaded"}, + {ID: "ups_%s_ups.status.TRIM", Name: "trim_input_voltage"}, + {ID: "ups_%s_ups.status.BOOST", Name: "boost_input_voltage"}, + {ID: "ups_%s_ups.status.FSD", Name: "forced_shutdown"}, + {ID: "ups_%s_ups.status.other", Name: "other"}, + }, + } + upsTemperatureChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.temperature", + Title: "UPS temperature", + Units: "Celsius", + Fam: "ups", + Ctx: "upsd.ups_temperature", + Priority: prioUpsTemperature, + Dims: module.Dims{ + {ID: "ups_%s_ups.temperature", Name: "temperature", Div: varPrecision}, + }, + } +) + +var ( + upsBatteryChargePercentChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.battery_charge_percentage", + Title: "UPS Battery charge", + Units: "percentage", + Fam: "battery", + Ctx: "upsd.ups_battery_charge", + Priority: prioBatteryCharge, + Type: module.Area, + Dims: module.Dims{ + {ID: "ups_%s_battery.charge", Name: "charge", Div: varPrecision}, + }, + } + upsBatteryEstimatedRuntimeChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.battery_estimated_runtime", + Title: "UPS Battery estimated runtime", + Units: "seconds", + Fam: "battery", + Ctx: "upsd.ups_battery_estimated_runtime", + Priority: prioBatteryEstimatedRuntime, + Dims: module.Dims{ + {ID: "ups_%s_battery.runtime", Name: "runtime", Div: varPrecision}, + }, + } + upsBatteryVoltageChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.battery_voltage", + Title: "UPS Battery voltage", + Units: "Volts", + Fam: "battery", + Ctx: "upsd.ups_battery_voltage", + Priority: prioBatteryVoltage, + Dims: module.Dims{ + {ID: "ups_%s_battery.voltage", Name: "voltage", Div: varPrecision}, + }, + } + upsBatteryVoltageNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.battery_voltage_nominal", + Title: "UPS Battery voltage nominal", + Units: "Volts", + Fam: "battery", + Ctx: "upsd.ups_battery_voltage_nominal", + Priority: prioBatteryVoltageNominal, + Dims: module.Dims{ + {ID: "ups_%s_battery.voltage.nominal", Name: "nominal_voltage", Div: varPrecision}, + }, + } +) + +var ( + upsInputVoltageChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_voltage", + Title: "UPS Input voltage", + Units: "Volts", + Fam: "input", + Ctx: "upsd.ups_input_voltage", + Priority: prioInputVoltage, + Dims: module.Dims{ + {ID: "ups_%s_input.voltage", Name: "voltage", Div: varPrecision}, + }, + } + upsInputVoltageNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_voltage_nominal", + Title: "UPS Input voltage nominal", + Units: "Volts", + Fam: "input", + Ctx: "upsd.ups_input_voltage_nominal", + Priority: prioInputVoltageNominal, + Dims: module.Dims{ + {ID: "ups_%s_input.voltage.nominal", Name: "nominal_voltage", Div: varPrecision}, + }, + } + upsInputCurrentChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_current", + Title: "UPS Input current", + Units: "Ampere", + Fam: "input", + Ctx: "upsd.ups_input_current", + Priority: prioInputCurrent, + Dims: module.Dims{ + {ID: "ups_%s_input.current", Name: "current", Div: varPrecision}, + }, + } + upsInputCurrentNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_current_nominal", + Title: "UPS Input current nominal", + Units: "Ampere", + Fam: "input", + Ctx: "upsd.ups_input_current_nominal", + Priority: prioInputCurrentNominal, + Dims: module.Dims{ + {ID: "ups_%s_input.current.nominal", Name: "nominal_current", Div: varPrecision}, + }, + } + upsInputFrequencyChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_frequency", + Title: "UPS Input frequency", + Units: "Hz", + Fam: "input", + Ctx: "upsd.ups_input_frequency", + Priority: prioInputFrequency, + Dims: module.Dims{ + {ID: "ups_%s_input.frequency", Name: "frequency", Div: varPrecision}, + }, + } + upsInputFrequencyNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.input_frequency_nominal", + Title: "UPS Input frequency nominal", + Units: "Hz", + Fam: "input", + Ctx: "upsd.ups_input_frequency_nominal", + Priority: prioInputFrequencyNominal, + Dims: module.Dims{ + {ID: "ups_%s_input.frequency.nominal", Name: "nominal_frequency", Div: varPrecision}, + }, + } +) + +var ( + upsOutputVoltageChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_voltage", + Title: "UPS Output voltage", + Units: "Volts", + Fam: "output", + Ctx: "upsd.ups_output_voltage", + Priority: prioOutputVoltage, + Dims: module.Dims{ + {ID: "ups_%s_output.voltage", Name: "voltage", Div: varPrecision}, + }, + } + upsOutputVoltageNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_voltage_nominal", + Title: "UPS Output voltage nominal", + Units: "Volts", + Fam: "output", + Ctx: "upsd.ups_output_voltage_nominal", + Priority: prioOutputVoltageNominal, + Dims: module.Dims{ + {ID: "ups_%s_output.voltage.nominal", Name: "nominal_voltage", Div: varPrecision}, + }, + } + upsOutputCurrentChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_current", + Title: "UPS Output current", + Units: "Ampere", + Fam: "output", + Ctx: "upsd.ups_output_current", + Priority: prioOutputCurrent, + Dims: module.Dims{ + {ID: "ups_%s_output.current", Name: "current", Div: varPrecision}, + }, + } + upsOutputCurrentNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_current_nominal", + Title: "UPS Output current nominal", + Units: "Ampere", + Fam: "output", + Ctx: "upsd.ups_output_current_nominal", + Priority: prioOutputCurrentNominal, + Dims: module.Dims{ + {ID: "ups_%s_output.current.nominal", Name: "nominal_current", Div: varPrecision}, + }, + } + upsOutputFrequencyChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_frequency", + Title: "UPS Output frequency", + Units: "Hz", + Fam: "output", + Ctx: "upsd.ups_output_frequency", + Priority: prioOutputFrequency, + Dims: module.Dims{ + {ID: "ups_%s_output.frequency", Name: "frequency", Div: varPrecision}, + }, + } + upsOutputFrequencyNominalChartTmpl = module.Chart{ + IDSep: true, + ID: "%s.output_frequency_nominal", + Title: "UPS Output frequency nominal", + Units: "Hz", + Fam: "output", + Ctx: "upsd.ups_output_frequency_nominal", + Priority: prioOutputFrequencyNominal, + Dims: module.Dims{ + {ID: "ups_%s_output.frequency.nominal", Name: "nominal_frequency", Div: varPrecision}, + }, + } +) + +func (u *Upsd) addUPSCharts(ups upsUnit) { + charts := upsChartsTmpl.Copy() + + var removed []string + for _, v := range []struct{ v, id string }{ + {varBatteryVoltage, upsBatteryVoltageChartTmpl.ID}, + {varBatteryVoltageNominal, upsBatteryVoltageNominalChartTmpl.ID}, + + {varUpsTemperature, upsTemperatureChartTmpl.ID}, + + {varInputVoltage, upsInputVoltageChartTmpl.ID}, + {varInputVoltageNominal, upsInputVoltageNominalChartTmpl.ID}, + {varInputCurrent, upsInputCurrentChartTmpl.ID}, + {varInputCurrentNominal, upsInputCurrentNominalChartTmpl.ID}, + {varInputFrequency, upsInputFrequencyChartTmpl.ID}, + {varInputFrequencyNominal, upsInputFrequencyNominalChartTmpl.ID}, + + {varOutputVoltage, upsOutputVoltageChartTmpl.ID}, + {varOutputVoltageNominal, upsOutputVoltageNominalChartTmpl.ID}, + {varOutputCurrent, upsOutputCurrentChartTmpl.ID}, + {varOutputCurrentNominal, upsOutputCurrentNominalChartTmpl.ID}, + {varOutputFrequency, upsOutputFrequencyChartTmpl.ID}, + {varOutputFrequencyNominal, upsOutputFrequencyNominalChartTmpl.ID}, + } { + if !hasVar(ups.vars, v.v) { + removed = append(removed, v.v) + _ = charts.Remove(v.id) + } + } + + u.Debugf("UPS '%s' no metrics: %v", ups.name, removed) + + name := cleanUpsName(ups.name) + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, name) + chart.Labels = []module.Label{ + {Key: "ups_name", Value: ups.name}, + {Key: "battery_type", Value: ups.vars[varBatteryType]}, + {Key: "device_model", Value: ups.vars[varDeviceModel]}, + {Key: "device_serial", Value: ups.vars[varDeviceSerial]}, + {Key: "device_manufacturer", Value: ups.vars[varDeviceMfr]}, + {Key: "device_type", Value: ups.vars[varDeviceType]}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, ups.name) + } + } + + if err := u.Charts().Add(*charts...); err != nil { + u.Warning(err) + } +} + +func (u *Upsd) removeUPSCharts(name string) { + name = cleanUpsName(name) + for _, chart := range *u.Charts() { + if strings.HasPrefix(chart.ID, name) { + chart.MarkRemove() + chart.MarkNotCreated() + } + } +} + +func cleanUpsName(name string) string { + name = strings.ReplaceAll(name, " ", "_") + name = strings.ReplaceAll(name, ".", "_") + return name +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/client.go b/src/go/collectors/go.d.plugin/modules/upsd/client.go new file mode 100644 index 000000000..a1b8f288e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/client.go @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +import ( + "encoding/csv" + "errors" + "fmt" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/socket" +) + +const ( + commandUsername = "USERNAME %s" + commandPassword = "PASSWORD %s" + commandListUPS = "LIST UPS" + commandListVar = "LIST VAR %s" + commandLogout = "LOGOUT" +) + +// https://github.com/networkupstools/nut/blob/81fca30b2998fa73085ce4654f075605ff0b9e01/docs/net-protocol.txt#L647 +var errUpsdCommand = errors.New("upsd command error") + +type upsUnit struct { + name string + vars map[string]string +} + +func newUpsdConn(conf Config) upsdConn { + return &upsdClient{conn: socket.New(socket.Config{ + ConnectTimeout: conf.Timeout.Duration(), + ReadTimeout: conf.Timeout.Duration(), + WriteTimeout: conf.Timeout.Duration(), + Address: conf.Address, + })} +} + +type upsdClient struct { + conn socket.Client +} + +func (c *upsdClient) connect() error { + return c.conn.Connect() +} + +func (c *upsdClient) disconnect() error { + _, _ = c.sendCommand(commandLogout) + return c.conn.Disconnect() +} + +func (c *upsdClient) authenticate(username, password string) error { + cmd := fmt.Sprintf(commandUsername, username) + resp, err := c.sendCommand(cmd) + if err != nil { + return err + } + if resp[0] != "OK" { + return errors.New("authentication failed: invalid username") + } + + cmd = fmt.Sprintf(commandPassword, password) + resp, err = c.sendCommand(cmd) + if err != nil { + return err + } + if resp[0] != "OK" { + return errors.New("authentication failed: invalid password") + } + + return nil +} + +func (c *upsdClient) upsUnits() ([]upsUnit, error) { + resp, err := c.sendCommand(commandListUPS) + if err != nil { + return nil, err + } + + var upsNames []string + + for _, v := range resp { + if !strings.HasPrefix(v, "UPS ") { + continue + } + parts := splitLine(v) + if len(parts) < 2 { + continue + } + name := parts[1] + upsNames = append(upsNames, name) + } + + var upsUnits []upsUnit + + for _, name := range upsNames { + cmd := fmt.Sprintf(commandListVar, name) + resp, err := c.sendCommand(cmd) + if err != nil { + return nil, err + } + + ups := upsUnit{ + name: name, + vars: make(map[string]string), + } + + upsUnits = append(upsUnits, ups) + + for _, v := range resp { + if !strings.HasPrefix(v, "VAR ") { + continue + } + parts := splitLine(v) + if len(parts) < 4 { + continue + } + n, v := parts[2], parts[3] + ups.vars[n] = v + } + } + + return upsUnits, nil +} + +func (c *upsdClient) sendCommand(cmd string) ([]string, error) { + var resp []string + var errMsg string + endLine := getEndLine(cmd) + + err := c.conn.Command(cmd+"\n", func(bytes []byte) bool { + line := string(bytes) + resp = append(resp, line) + + if strings.HasPrefix(line, "ERR ") { + errMsg = strings.TrimPrefix(line, "ERR ") + } + + return line != endLine && errMsg == "" + }) + if err != nil { + return nil, err + } + if errMsg != "" { + return nil, fmt.Errorf("%w: %s (cmd: '%s')", errUpsdCommand, errMsg, cmd) + } + + return resp, nil +} + +func getEndLine(cmd string) string { + px, _, _ := strings.Cut(cmd, " ") + + switch px { + case "USERNAME", "PASSWORD", "VER": + return "OK" + } + return fmt.Sprintf("END %s", cmd) +} + +func splitLine(s string) []string { + r := csv.NewReader(strings.NewReader(s)) + r.Comma = ' ' + + parts, _ := r.Read() + + return parts +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/collect.go b/src/go/collectors/go.d.plugin/modules/upsd/collect.go new file mode 100644 index 000000000..39e3d1b55 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/collect.go @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +import ( + "errors" + "strconv" + "strings" +) + +func (u *Upsd) collect() (map[string]int64, error) { + if u.conn == nil { + conn, err := u.establishConnection() + if err != nil { + return nil, err + } + u.conn = conn + } + + upsUnits, err := u.conn.upsUnits() + if err != nil { + if !errors.Is(err, errUpsdCommand) { + _ = u.conn.disconnect() + u.conn = nil + } + return nil, err + } + + u.Debugf("found %d UPS units", len(upsUnits)) + + mx := make(map[string]int64) + + u.collectUPSUnits(mx, upsUnits) + + return mx, nil +} + +func (u *Upsd) establishConnection() (upsdConn, error) { + conn := u.newUpsdConn(u.Config) + + if err := conn.connect(); err != nil { + return nil, err + } + + if u.Username != "" && u.Password != "" { + if err := conn.authenticate(u.Username, u.Password); err != nil { + _ = conn.disconnect() + return nil, err + } + } + + return conn, nil +} + +func (u *Upsd) collectUPSUnits(mx map[string]int64, upsUnits []upsUnit) { + seen := make(map[string]bool) + + for _, ups := range upsUnits { + seen[ups.name] = true + u.Debugf("collecting metrics UPS '%s'", ups.name) + + if !u.upsUnits[ups.name] { + u.upsUnits[ups.name] = true + u.addUPSCharts(ups) + } + + writeVar(mx, ups, varBatteryCharge) + writeVar(mx, ups, varBatteryRuntime) + writeVar(mx, ups, varBatteryVoltage) + writeVar(mx, ups, varBatteryVoltageNominal) + + writeVar(mx, ups, varInputVoltage) + writeVar(mx, ups, varInputVoltageNominal) + writeVar(mx, ups, varInputCurrent) + writeVar(mx, ups, varInputCurrentNominal) + writeVar(mx, ups, varInputFrequency) + writeVar(mx, ups, varInputFrequencyNominal) + + writeVar(mx, ups, varOutputVoltage) + writeVar(mx, ups, varOutputVoltageNominal) + writeVar(mx, ups, varOutputCurrent) + writeVar(mx, ups, varOutputCurrentNominal) + writeVar(mx, ups, varOutputFrequency) + writeVar(mx, ups, varOutputFrequencyNominal) + + writeVar(mx, ups, varUpsLoad) + writeVar(mx, ups, varUpsRealPowerNominal) + writeVar(mx, ups, varUpsTemperature) + writeUpsLoadUsage(mx, ups) + writeUpsStatus(mx, ups) + } + + for name := range u.upsUnits { + if !seen[name] { + delete(u.upsUnits, name) + u.removeUPSCharts(name) + } + } +} + +func writeVar(mx map[string]int64, ups upsUnit, v string) { + s, ok := ups.vars[v] + if !ok { + return + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return + } + mx[prefix(ups)+v] = int64(n * varPrecision) +} + +func writeUpsLoadUsage(mx map[string]int64, ups upsUnit) { + if hasVar(ups.vars, varUpsRealPower) { + pow, _ := strconv.ParseFloat(ups.vars[varUpsRealPower], 64) + mx[prefix(ups)+"ups.load.usage"] = int64(pow * varPrecision) + return + } + + if !hasVar(ups.vars, varUpsLoad) || !hasVar(ups.vars, varUpsRealPowerNominal) { + return + } + load, err := strconv.ParseFloat(ups.vars[varUpsLoad], 64) + if err != nil { + return + } + nomPower, err := strconv.ParseFloat(ups.vars[varUpsRealPowerNominal], 64) + if err != nil || nomPower == 0 { + return + } + mx[prefix(ups)+"ups.load.usage"] = int64((load / 100 * nomPower) * varPrecision) +} + +// https://networkupstools.org/docs/developer-guide.chunked/ar01s04.html#_status_data +var upsStatuses = map[string]bool{ + "OL": true, + "OB": true, + "LB": true, + "HB": true, + "RB": true, + "CHRG": true, + "DISCHRG": true, + "BYPASS": true, + "CAL": true, + "OFF": true, + "OVER": true, + "TRIM": true, + "BOOST": true, + "FSD": true, +} + +func writeUpsStatus(mx map[string]int64, ups upsUnit) { + if !hasVar(ups.vars, varUpsStatus) { + return + } + + px := prefix(ups) + "ups.status." + + for st := range upsStatuses { + mx[px+st] = 0 + } + mx[px+"other"] = 0 + + for _, st := range strings.Split(ups.vars[varUpsStatus], " ") { + if _, ok := upsStatuses[st]; ok { + mx[px+st] = 1 + } else { + mx[px+"other"] = 1 + } + } +} + +func hasVar(vars map[string]string, v string) bool { + _, ok := vars[v] + return ok +} + +func prefix(ups upsUnit) string { + return "ups_" + ups.name + "_" +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/config_schema.json b/src/go/collectors/go.d.plugin/modules/upsd/config_schema.json new file mode 100644 index 000000000..564c0179c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/config_schema.json @@ -0,0 +1,85 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UPSd collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "address": { + "title": "Address", + "description": "The IP address and port where the UPSd daemon listens for connections.", + "type": "string", + "default": "127.0.0.1:3493" + }, + "timeout": { + "title": "Timeout", + "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "username": { + "title": "Username", + "description": "The username for authentication.", + "type": "string" + }, + "password": { + "title": "Password", + "description": "The password for authentication.", + "type": "string" + } + }, + "required": [ + "address" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + }, + "dependencies": { + "username": [ + "password" + ], + "password": [ + "username" + ] + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "password": { + "ui:widget": "password" + }, + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "address", + "timeout" + ] + }, + { + "title": "Auth", + "fields": [ + "username", + "password" + ] + } + ] + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/integrations/ups_nut.md b/src/go/collectors/go.d.plugin/modules/upsd/integrations/ups_nut.md new file mode 100644 index 000000000..c02eafc70 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/integrations/ups_nut.md @@ -0,0 +1,211 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/upsd/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/upsd/metadata.yaml" +sidebar_label: "UPS (NUT)" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/UPS" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# UPS (NUT) + + +<img src="https://netdata.cloud/img/plug-circle-bolt.svg" width="150"/> + + +Plugin: go.d.plugin +Module: upsd + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors Uninterruptible Power Supplies by polling the UPS daemon using the NUT network protocol. + + + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per ups + +These metrics refer to the UPS unit. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| ups_name | UPS name. | +| battery_type | Battery type (chemistry). "battery.type" variable value. | +| device_model | Device model. "device.mode" variable value. | +| device_serial | Device serial number. "device.serial" variable value. | +| device_manufacturer | Device manufacturer. "device.mfr" variable value. | +| device_type | Device type (ups, pdu, scd, psu, ats). "device.type" variable value. | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| upsd.ups_load | load | percentage | +| upsd.ups_load_usage | load_usage | Watts | +| upsd.ups_status | on_line, on_battery, low_battery, high_battery, replace_battery, charging, discharging, bypass, calibration, offline, overloaded, trim_input_voltage, boost_input_voltage, forced_shutdown, other | status | +| upsd.ups_temperature | temperature | Celsius | +| upsd.ups_battery_charge | charge | percentage | +| upsd.ups_battery_estimated_runtime | runtime | seconds | +| upsd.ups_battery_voltage | voltage | Volts | +| upsd.ups_battery_voltage_nominal | nominal_voltage | Volts | +| upsd.ups_input_voltage | voltage | Volts | +| upsd.ups_input_voltage_nominal | nominal_voltage | Volts | +| upsd.ups_input_current | current | Ampere | +| upsd.ups_input_current_nominal | nominal_current | Ampere | +| upsd.ups_input_frequency | frequency | Hz | +| upsd.ups_input_frequency_nominal | nominal_frequency | Hz | +| upsd.ups_output_voltage | voltage | Volts | +| upsd.ups_output_voltage_nominal | nominal_voltage | Volts | +| upsd.ups_output_current | current | Ampere | +| upsd.ups_output_current_nominal | nominal_current | Ampere | +| upsd.ups_output_frequency | frequency | Hz | +| upsd.ups_output_frequency_nominal | nominal_frequency | Hz | + + + +## Alerts + + +The following alerts are available: + +| Alert name | On metric | Description | +|:------------|:----------|:------------| +| [ upsd_10min_ups_load ](https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf) | upsd.ups_load | UPS ${label:ups_name} average load over the last 10 minutes | +| [ upsd_ups_battery_charge ](https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf) | upsd.ups_battery_charge | UPS ${label:ups_name} average battery charge over the last minute | +| [ upsd_ups_last_collected_secs ](https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf) | upsd.ups_load | UPS ${label:ups_name} number of seconds since the last successful data collection | + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/upsd.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/upsd.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| address | UPS daemon address in IP:PORT format. | 127.0.0.1:3493 | yes | +| timeout | Connection/read/write timeout in seconds. The timeout includes name resolution, if required. | 2 | no | + +</details> + +#### Examples + +##### Basic + +A basic example configuration. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + address: 127.0.0.1:3493 + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + address: 127.0.0.1:3493 + + - name: remote + address: 203.0.113.0:3493 + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `upsd` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m upsd + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/upsd/metadata.yaml b/src/go/collectors/go.d.plugin/modules/upsd/metadata.yaml new file mode 100644 index 000000000..070b33852 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/metadata.yaml @@ -0,0 +1,264 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-upsd + plugin_name: go.d.plugin + module_name: upsd + monitored_instance: + name: UPS (NUT) + link: "" + icon_filename: plug-circle-bolt.svg + categories: + - data-collection.ups + keywords: + - ups + - nut + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors Uninterruptible Power Supplies by polling the UPS daemon using the NUT network protocol. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/upsd.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: address + description: UPS daemon address in IP:PORT format. + default_value: 127.0.0.1:3493 + required: true + - name: timeout + description: Connection/read/write timeout in seconds. The timeout includes name resolution, if required. + default_value: 2 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: A basic example configuration. + config: | + jobs: + - name: local + address: 127.0.0.1:3493 + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + address: 127.0.0.1:3493 + + - name: remote + address: 203.0.113.0:3493 + troubleshooting: + problems: + list: [] + alerts: + - name: upsd_10min_ups_load + metric: upsd.ups_load + info: "UPS ${label:ups_name} average load over the last 10 minutes" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf + - name: upsd_ups_battery_charge + metric: upsd.ups_battery_charge + info: "UPS ${label:ups_name} average battery charge over the last minute" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf + - name: upsd_ups_last_collected_secs + metric: upsd.ups_load + info: "UPS ${label:ups_name} number of seconds since the last successful data collection" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/upsd.conf + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: ups + description: These metrics refer to the UPS unit. + labels: + - name: ups_name + description: UPS name. + - name: battery_type + description: Battery type (chemistry). "battery.type" variable value. + - name: device_model + description: Device model. "device.mode" variable value. + - name: device_serial + description: Device serial number. "device.serial" variable value. + - name: device_manufacturer + description: Device manufacturer. "device.mfr" variable value. + - name: device_type + description: Device type (ups, pdu, scd, psu, ats). "device.type" variable value. + metrics: + - name: upsd.ups_load + description: UPS load + unit: percentage + chart_type: area + dimensions: + - name: load + - name: upsd.ups_load_usage + description: UPS load usage (power output) + unit: Watts + chart_type: line + dimensions: + - name: load_usage + - name: upsd.ups_status + description: UPS status + unit: status + chart_type: line + dimensions: + - name: on_line + - name: on_battery + - name: low_battery + - name: high_battery + - name: replace_battery + - name: charging + - name: discharging + - name: bypass + - name: calibration + - name: offline + - name: overloaded + - name: trim_input_voltage + - name: boost_input_voltage + - name: forced_shutdown + - name: other + - name: upsd.ups_temperature + description: UPS temperature + unit: Celsius + chart_type: line + dimensions: + - name: temperature + - name: upsd.ups_battery_charge + description: UPS Battery charge + unit: percentage + chart_type: area + dimensions: + - name: charge + - name: upsd.ups_battery_estimated_runtime + description: UPS Battery estimated runtime + unit: seconds + chart_type: line + dimensions: + - name: runtime + - name: upsd.ups_battery_voltage + description: UPS Battery voltage + unit: Volts + chart_type: line + dimensions: + - name: voltage + - name: upsd.ups_battery_voltage_nominal + description: UPS Battery voltage nominal + unit: Volts + chart_type: line + dimensions: + - name: nominal_voltage + - name: upsd.ups_input_voltage + description: UPS Input voltage + unit: Volts + chart_type: line + dimensions: + - name: voltage + - name: upsd.ups_input_voltage_nominal + description: UPS Input voltage nominal + unit: Volts + chart_type: line + dimensions: + - name: nominal_voltage + - name: upsd.ups_input_current + description: UPS Input current + unit: Ampere + chart_type: line + dimensions: + - name: current + - name: upsd.ups_input_current_nominal + description: UPS Input current nominal + unit: Ampere + chart_type: line + dimensions: + - name: nominal_current + - name: upsd.ups_input_frequency + description: UPS Input frequency + unit: Hz + chart_type: line + dimensions: + - name: frequency + - name: upsd.ups_input_frequency_nominal + description: UPS Input frequency nominal + unit: Hz + chart_type: line + dimensions: + - name: nominal_frequency + - name: upsd.ups_output_voltage + description: UPS Output voltage + unit: Volts + chart_type: line + dimensions: + - name: voltage + - name: upsd.ups_output_voltage_nominal + description: UPS Output voltage nominal + unit: Volts + chart_type: line + dimensions: + - name: nominal_voltage + - name: upsd.ups_output_current + description: UPS Output current + unit: Ampere + chart_type: line + dimensions: + - name: current + - name: upsd.ups_output_current_nominal + description: UPS Output current nominal + unit: Ampere + chart_type: line + dimensions: + - name: nominal_current + - name: upsd.ups_output_frequency + description: UPS Output frequency + unit: Hz + chart_type: line + dimensions: + - name: frequency + - name: upsd.ups_output_frequency_nominal + description: UPS Output frequency nominal + unit: Hz + chart_type: line + dimensions: + - name: nominal_frequency diff --git a/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.json b/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.json new file mode 100644 index 000000000..ab7a8654c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.json @@ -0,0 +1,7 @@ +{ + "update_every": 123, + "address": "ok", + "username": "ok", + "password": "ok", + "timeout": 123.123 +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.yaml new file mode 100644 index 000000000..276370415 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/testdata/config.yaml @@ -0,0 +1,5 @@ +update_every: 123 +address: "ok" +username: "ok" +password: "ok" +timeout: 123.123 diff --git a/src/go/collectors/go.d.plugin/modules/upsd/upsd.go b/src/go/collectors/go.d.plugin/modules/upsd/upsd.go new file mode 100644 index 000000000..be734bc5a --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/upsd.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("upsd", module.Creator{ + JobConfigSchema: configSchema, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Upsd { + return &Upsd{ + Config: Config{ + Address: "127.0.0.1:3493", + Timeout: web.Duration(time.Second * 2), + }, + newUpsdConn: newUpsdConn, + charts: &module.Charts{}, + upsUnits: make(map[string]bool), + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Address string `yaml:"address" json:"address"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` + Username string `yaml:"username,omitempty" json:"username"` + Password string `yaml:"password,omitempty" json:"password"` +} + +type ( + Upsd struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + conn upsdConn + newUpsdConn func(Config) upsdConn + + upsUnits map[string]bool + } + + upsdConn interface { + connect() error + disconnect() error + authenticate(string, string) error + upsUnits() ([]upsUnit, error) + } +) + +func (u *Upsd) Configuration() any { + return u.Config +} + +func (u *Upsd) Init() error { + if u.Address == "" { + u.Error("config: 'address' not set") + return errors.New("address not set") + } + + return nil +} + +func (u *Upsd) Check() error { + mx, err := u.collect() + if err != nil { + u.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (u *Upsd) Charts() *module.Charts { + return u.charts +} + +func (u *Upsd) Collect() map[string]int64 { + mx, err := u.collect() + if err != nil { + u.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (u *Upsd) Cleanup() { + if u.conn == nil { + return + } + if err := u.conn.disconnect(); err != nil { + u.Warningf("error on disconnect: %v", err) + } + u.conn = nil +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/upsd_test.go b/src/go/collectors/go.d.plugin/modules/upsd/upsd_test.go new file mode 100644 index 000000000..1dffdd0f5 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/upsd_test.go @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestUpsd_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Upsd{}, dataConfigJSON, dataConfigYAML) +} + +func TestUpsd_Cleanup(t *testing.T) { + upsd := New() + + require.NotPanics(t, upsd.Cleanup) + + mock := prepareMockConnOK() + upsd.newUpsdConn = func(Config) upsdConn { return mock } + + require.NoError(t, upsd.Init()) + _ = upsd.Collect() + require.NotPanics(t, upsd.Cleanup) + assert.True(t, mock.calledDisconnect) +} + +func TestUpsd_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "success on default config": { + wantFail: false, + config: New().Config, + }, + "fails when 'address' option not set": { + wantFail: true, + config: Config{Address: ""}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + upsd := New() + upsd.Config = test.config + + if test.wantFail { + assert.Error(t, upsd.Init()) + } else { + assert.NoError(t, upsd.Init()) + } + }) + } +} + +func TestUpsd_Check(t *testing.T) { + tests := map[string]struct { + prepareUpsd func() *Upsd + prepareMock func() *mockUpsdConn + wantFail bool + }{ + "successful data collection": { + wantFail: false, + prepareUpsd: New, + prepareMock: prepareMockConnOK, + }, + "error on connect()": { + wantFail: true, + prepareUpsd: New, + prepareMock: prepareMockConnErrOnConnect, + }, + "error on authenticate()": { + wantFail: true, + prepareUpsd: func() *Upsd { + upsd := New() + upsd.Username = "user" + upsd.Password = "pass" + return upsd + }, + prepareMock: prepareMockConnErrOnAuthenticate, + }, + "error on upsList()": { + wantFail: true, + prepareUpsd: New, + prepareMock: prepareMockConnErrOnUpsUnits, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + upsd := test.prepareUpsd() + upsd.newUpsdConn = func(Config) upsdConn { return test.prepareMock() } + + require.NoError(t, upsd.Init()) + + if test.wantFail { + assert.Error(t, upsd.Check()) + } else { + assert.NoError(t, upsd.Check()) + } + }) + } +} + +func TestUpsd_Charts(t *testing.T) { + upsd := New() + require.NoError(t, upsd.Init()) + assert.NotNil(t, upsd.Charts()) +} + +func TestUpsd_Collect(t *testing.T) { + tests := map[string]struct { + prepareUpsd func() *Upsd + prepareMock func() *mockUpsdConn + wantCollected map[string]int64 + wantCharts int + wantConnConnect bool + wantConnDisconnect bool + wantConnAuthenticate bool + }{ + "successful data collection": { + prepareUpsd: New, + prepareMock: prepareMockConnOK, + wantCollected: map[string]int64{ + "ups_cp1500_battery.charge": 10000, + "ups_cp1500_battery.runtime": 489000, + "ups_cp1500_battery.voltage": 2400, + "ups_cp1500_battery.voltage.nominal": 2400, + "ups_cp1500_input.voltage": 22700, + "ups_cp1500_input.voltage.nominal": 23000, + "ups_cp1500_output.voltage": 26000, + "ups_cp1500_ups.load": 800, + "ups_cp1500_ups.load.usage": 4300, + "ups_cp1500_ups.realpower.nominal": 90000, + "ups_cp1500_ups.status.BOOST": 0, + "ups_cp1500_ups.status.BYPASS": 0, + "ups_cp1500_ups.status.CAL": 0, + "ups_cp1500_ups.status.CHRG": 0, + "ups_cp1500_ups.status.DISCHRG": 0, + "ups_cp1500_ups.status.FSD": 0, + "ups_cp1500_ups.status.HB": 0, + "ups_cp1500_ups.status.LB": 0, + "ups_cp1500_ups.status.OB": 0, + "ups_cp1500_ups.status.OFF": 0, + "ups_cp1500_ups.status.OL": 1, + "ups_cp1500_ups.status.OVER": 0, + "ups_cp1500_ups.status.RB": 0, + "ups_cp1500_ups.status.TRIM": 0, + "ups_cp1500_ups.status.other": 0, + "ups_pr3000_battery.charge": 10000, + "ups_pr3000_battery.runtime": 110800, + "ups_pr3000_battery.voltage": 5990, + "ups_pr3000_battery.voltage.nominal": 4800, + "ups_pr3000_input.voltage": 22500, + "ups_pr3000_input.voltage.nominal": 23000, + "ups_pr3000_output.voltage": 22500, + "ups_pr3000_ups.load": 2800, + "ups_pr3000_ups.load.usage": 84000, + "ups_pr3000_ups.realpower.nominal": 300000, + "ups_pr3000_ups.status.BOOST": 0, + "ups_pr3000_ups.status.BYPASS": 0, + "ups_pr3000_ups.status.CAL": 0, + "ups_pr3000_ups.status.CHRG": 0, + "ups_pr3000_ups.status.DISCHRG": 0, + "ups_pr3000_ups.status.FSD": 0, + "ups_pr3000_ups.status.HB": 0, + "ups_pr3000_ups.status.LB": 0, + "ups_pr3000_ups.status.OB": 0, + "ups_pr3000_ups.status.OFF": 0, + "ups_pr3000_ups.status.OL": 1, + "ups_pr3000_ups.status.OVER": 0, + "ups_pr3000_ups.status.RB": 0, + "ups_pr3000_ups.status.TRIM": 0, + "ups_pr3000_ups.status.other": 0, + }, + wantCharts: 20, + wantConnConnect: true, + wantConnDisconnect: false, + wantConnAuthenticate: false, + }, + "error on connect()": { + prepareUpsd: New, + prepareMock: prepareMockConnErrOnConnect, + wantCollected: nil, + wantCharts: 0, + wantConnConnect: true, + wantConnDisconnect: false, + wantConnAuthenticate: false, + }, + "error on authenticate()": { + prepareUpsd: func() *Upsd { + upsd := New() + upsd.Username = "user" + upsd.Password = "pass" + return upsd + }, + prepareMock: prepareMockConnErrOnAuthenticate, + wantCollected: nil, + wantCharts: 0, + wantConnConnect: true, + wantConnDisconnect: true, + wantConnAuthenticate: true, + }, + "err on upsList()": { + prepareUpsd: New, + prepareMock: prepareMockConnErrOnUpsUnits, + wantCollected: nil, + wantCharts: 0, + wantConnConnect: true, + wantConnDisconnect: true, + wantConnAuthenticate: false, + }, + "command err on upsList() (unknown ups)": { + prepareUpsd: New, + prepareMock: prepareMockConnCommandErrOnUpsUnits, + wantCollected: nil, + wantCharts: 0, + wantConnConnect: true, + wantConnDisconnect: false, + wantConnAuthenticate: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + upsd := test.prepareUpsd() + require.NoError(t, upsd.Init()) + + mock := test.prepareMock() + upsd.newUpsdConn = func(Config) upsdConn { return mock } + + mx := upsd.Collect() + + assert.Equal(t, test.wantCollected, mx) + assert.Equalf(t, test.wantCharts, len(*upsd.Charts()), "number of charts") + if len(test.wantCollected) > 0 { + ensureCollectedHasAllChartsDims(t, upsd, mx) + } + assert.Equalf(t, test.wantConnConnect, mock.calledConnect, "calledConnect") + assert.Equalf(t, test.wantConnDisconnect, mock.calledDisconnect, "calledDisconnect") + assert.Equal(t, test.wantConnAuthenticate, mock.calledAuthenticate, "calledAuthenticate") + }) + } +} + +func ensureCollectedHasAllChartsDims(t *testing.T, upsd *Upsd, mx map[string]int64) { + for _, chart := range *upsd.Charts() { + if chart.Obsolete { + continue + } + for _, dim := range chart.Dims { + _, ok := mx[dim.ID] + assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := mx[v.ID] + assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID) + } + } +} + +func prepareMockConnOK() *mockUpsdConn { + return &mockUpsdConn{} +} + +func prepareMockConnErrOnConnect() *mockUpsdConn { + return &mockUpsdConn{errOnConnect: true} +} + +func prepareMockConnErrOnAuthenticate() *mockUpsdConn { + return &mockUpsdConn{errOnAuthenticate: true} +} + +func prepareMockConnErrOnUpsUnits() *mockUpsdConn { + return &mockUpsdConn{errOnUpsUnits: true} +} + +func prepareMockConnCommandErrOnUpsUnits() *mockUpsdConn { + return &mockUpsdConn{commandErrOnUpsUnits: true} +} + +type mockUpsdConn struct { + errOnConnect bool + errOnDisconnect bool + errOnAuthenticate bool + errOnUpsUnits bool + commandErrOnUpsUnits bool + + calledConnect bool + calledDisconnect bool + calledAuthenticate bool +} + +func (m *mockUpsdConn) connect() error { + m.calledConnect = true + if m.errOnConnect { + return errors.New("mock error on connect()") + } + return nil +} + +func (m *mockUpsdConn) disconnect() error { + m.calledDisconnect = true + if m.errOnDisconnect { + return errors.New("mock error on disconnect()") + } + return nil +} + +func (m *mockUpsdConn) authenticate(_, _ string) error { + m.calledAuthenticate = true + if m.errOnAuthenticate { + return errors.New("mock error on authenticate()") + } + return nil +} + +func (m *mockUpsdConn) upsUnits() ([]upsUnit, error) { + if m.errOnUpsUnits { + return nil, errors.New("mock error on upsUnits()") + } + if m.commandErrOnUpsUnits { + return nil, fmt.Errorf("%w: mock command error on upsUnits()", errUpsdCommand) + } + + upsUnits := []upsUnit{ + { + name: "pr3000", + vars: map[string]string{ + "battery.charge": "100", + "battery.charge.warning": "35", + "battery.mfr.date": "CPS", + "battery.runtime": "1108", + "battery.runtime.low": "300", + "battery.type": "PbAcid", + "battery.voltage": "59.9", + "battery.voltage.nominal": "48", + "device.mfr": "CPS", + "device.model": "PR3000ERT2U", + "device.serial": "P11MQ2000041", + "device.type": "ups", + "driver.name": "usbhid-ups", + "driver.parameter.pollfreq": "30", + "driver.parameter.pollinterval": "2", + "driver.parameter.port": "auto", + "driver.parameter.synchronous": "no", + "driver.version": "2.7.4", + "driver.version.data": "CyberPower HID 0.4", + "driver.version.internal": "0.41", + "input.voltage": "225.0", + "input.voltage.nominal": "230", + "output.voltage": "225.0", + "ups.beeper.status": "enabled", + "ups.delay.shutdown": "20", + "ups.delay.start": "30", + "ups.load": "28", + "ups.mfr": "CPS", + "ups.model": "PR3000ERT2U", + "ups.productid": "0601", + "ups.realpower.nominal": "3000", + "ups.serial": "P11MQ2000041", + "ups.status": "OL", + "ups.test.result": "No test initiated", + "ups.timer.shutdown": "0", + "ups.timer.start": "0", + "ups.vendorid": "0764", + }, + }, + { + name: "cp1500", + vars: map[string]string{ + "battery.charge": "100", + "battery.charge.low": "10", + "battery.charge.warning": "20", + "battery.mfr.date": "CPS", + "battery.runtime": "4890", + "battery.runtime.low": "300", + "battery.type": "PbAcid", + "battery.voltage": "24.0", + "battery.voltage.nominal": "24", + "device.mfr": "CPS", + "device.model": "CP1500EPFCLCD", + "device.serial": "CRMNO2000312", + "device.type": "ups", + "driver.name": "usbhid-ups", + "driver.parameter.bus": "001", + "driver.parameter.pollfreq": "30", + "driver.parameter.pollinterval": "2", + "driver.parameter.port": "auto", + "driver.parameter.product": "CP1500EPFCLCD", + "driver.parameter.productid": "0501", + "driver.parameter.serial": "CRMNO2000312", + "driver.parameter.synchronous": "no", + "driver.parameter.vendor": "CPS", + "driver.parameter.vendorid": "0764", + "driver.version": "2.7.4", + "driver.version.data": "CyberPower HID 0.4", + "driver.version.internal": "0.41", + "input.transfer.high": "260", + "input.transfer.low": "170", + "input.voltage": "227.0", + "input.voltage.nominal": "230", + "output.voltage": "260.0", + "ups.beeper.status": "enabled", + "ups.delay.shutdown": "20", + "ups.delay.start": "30", + "ups.load": "8", + "ups.mfr": "CPS", + "ups.model": "CP1500EPFCLCD", + "ups.productid": "0501", + "ups.realpower": "43", + "ups.realpower.nominal": "900", + "ups.serial": "CRMNO2000312", + "ups.status": "OL", + "ups.test.result": "No test initiated", + "ups.timer.shutdown": "-60", + "ups.timer.start": "-60", + "ups.vendorid": "0764", + }, + }, + } + + return upsUnits, nil +} diff --git a/src/go/collectors/go.d.plugin/modules/upsd/variables.go b/src/go/collectors/go.d.plugin/modules/upsd/variables.go new file mode 100644 index 000000000..9792e62b9 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/upsd/variables.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upsd + +const varPrecision = 100 + +// https://networkupstools.org/docs/developer-guide.chunked/apas02.html +const ( + varBatteryCharge = "battery.charge" + varBatteryRuntime = "battery.runtime" + varBatteryVoltage = "battery.voltage" + varBatteryVoltageNominal = "battery.voltage.nominal" + varBatteryType = "battery.type" + + varInputVoltage = "input.voltage" + varInputVoltageNominal = "input.voltage.nominal" + varInputCurrent = "input.current" + varInputCurrentNominal = "input.current.nominal" + varInputFrequency = "input.frequency" + varInputFrequencyNominal = "input.frequency.nominal" + + varOutputVoltage = "output.voltage" + varOutputVoltageNominal = "output.voltage.nominal" + varOutputCurrent = "output.current" + varOutputCurrentNominal = "output.current.nominal" + varOutputFrequency = "output.frequency" + varOutputFrequencyNominal = "output.frequency.nominal" + + varUpsLoad = "ups.load" + varUpsRealPower = "ups.realpower" + varUpsRealPowerNominal = "ups.realpower.nominal" + varUpsTemperature = "ups.temperature" + varUpsStatus = "ups.status" + + varDeviceModel = "device.model" + varDeviceSerial = "device.serial" + varDeviceMfr = "device.mfr" + varDeviceType = "device.type" +) |