Files
nmon/internal/librenms/modules.go
jleung 287fbb98bd Maintainability cleanup (core libraries): 223 lint findings to 0, oversized files split; fix apc-ups label columns and never poll text metrics
Migration files restructured with shared helpers; verified byte-identical SQLite schema vs previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 19:11:15 +00:00

173 lines
4.9 KiB
Go

package librenms
import (
"fmt"
"strconv"
)
// convertModules walks the modules: block of an os_discovery file.
func (im *importer) convertModules(mods map[string]any) {
for _, k := range sortedKeys(mods) {
sec := asMap(mods[k])
switch k {
case "processors":
for i, it := range asList(sec["data"]) {
im.processor(fmt.Sprintf("processors[%d]", i), asMap(it))
}
case "mempools":
for i, it := range asList(sec["data"]) {
im.pool(fmt.Sprintf("mempools[%d]", i), asMap(it), "mempool")
}
case "storage":
for i, it := range asList(sec["data"]) {
im.pool(fmt.Sprintf("storage[%d]", i), asMap(it), "storage")
}
case "sensors":
im.sensors(sec)
case "os":
im.skip("os", "version/hardware/serial extraction is device inventory text, not a metric")
default:
im.skip(k, "module not mapped")
}
}
}
func (im *importer) processor(src string, e map[string]any) {
if e == nil {
im.skip(src, "not a mapping")
return
}
sym := str(e["value"])
if sym == "" {
sym = str(e["oid"])
}
en := entry{src: src, e: e, valueSym: sym, numOID: str(e["num_oid"]), unit: "percent",
handled: handledKeys("oid", "value", "num_oid", "descr", "divisor", "multiplier", "skip_values", "precision")}
if p, ok := number(e["precision"]); ok && p != 0 && p != 1 {
// LibreNMS divides the raw reading by a positive precision and multiplies by a negative one
if p > 0 {
en.scale = 1 / p
} else {
en.scale = -p
}
en.extra = append(en.extra, fmt.Sprintf("scale from precision %s (LibreNMS divides by a positive precision); verify", fmtNum(p)))
}
im.addEntry(en)
}
// poolScale reads the unit conversion of a mempool (precision) or storage (units) block.
func poolScale(e map[string]any, kind string) (scale float64, extra []string) {
if kind == "mempool" {
if p, ok := number(e["precision"]); ok && p > 0 && p != 1 {
return p, []string{"scale from precision " + fmtNum(p) + " (raw units to bytes)"}
}
return 0, nil
}
u := e["units"]
if u == nil {
return 0, nil
}
n, ok := number(u)
switch {
case !ok || n <= 0:
return 0, []string{"units " + strconv.Quote(oneLine(fmt.Sprint(u))) +
" is read from the device in LibreNMS (allocation units); apply it as scale"}
case n != 1:
return n, []string{"scale from units " + fmtNum(n) + " (allocation unit to bytes)"}
}
return 0, nil
}
// poolHandled lists the keys pool consumes, so they are not reported as unmapped.
var poolHandled = []string{"oid", "descr", "skip_values", "precision", "units", "divisor", "multiplier"}
// pool converts mempools and storage blocks, which name several value objects.
func (im *importer) pool(src string, e map[string]any, kind string) {
if e == nil {
im.skip(src, "not a mapping")
return
}
handled := map[string]bool{}
for _, k := range poolHandled {
handled[k] = true
}
scale, extra := poolScale(e, kind)
found := false
for _, f := range []string{"percent_used", "used", "free", "total", "size"} {
v, has := e[f]
numKey := f + "_num_oid"
handled[f], handled[numKey] = true, true
if !has && e[numKey] == nil {
continue
}
unit, sc, ex := "bytes", scale, extra
if f == "percent_used" {
unit, sc, ex = "percent", 0, nil
}
sym := ""
if s, ok := v.(string); ok {
sym = s
} else if has {
// a constant (integer) total, not an SNMP object
im.skip(src+"."+f, "constant value %v, not an SNMP object", v)
continue
}
found = true
im.addEntry(entry{src: src + "." + f, e: e, valueSym: sym, numOID: str(e[numKey]), unit: unit, scale: sc,
handled: handled, extra: ex})
}
if !found {
im.skip(src, "no value object found")
}
}
var sensorHandled = handledKeys("oid", "value", "num_oid", "descr", "divisor", "multiplier", "skip_values", "states",
"skip_value_lt", "skip_value_gt", "user_func", "rrd_type")
func handledKeys(keys ...string) map[string]bool {
m := make(map[string]bool, len(keys))
for _, k := range keys {
m[k] = true
}
return m
}
func (im *importer) sensors(sec map[string]any) {
for _, class := range sortedKeys(sec) {
cs := asMap(sec[class])
if class == "additional_oids" {
im.skip("sensors.additional_oids", "helper OIDs read alongside sensors, not metrics")
continue
}
if cs == nil {
im.skip("sensors."+class, "not a mapping")
continue
}
unit, ok := sensorUnits[class]
var cnote []string
if !ok {
unit = "count"
cnote = []string{fmt.Sprintf("LibreNMS class %q has no unit in the profile vocabulary; count used, review", class)}
}
defaults := asMap(cs["options"])
for i, it := range asList(cs["data"]) {
src := fmt.Sprintf("sensors.%s[%d]", class, i)
e := asMap(it)
if e == nil {
im.skip(src, "not a mapping")
continue
}
sym := str(e["value"])
if sym == "" {
sym = str(e["oid"])
}
en := entry{src: src, e: e, valueSym: sym, numOID: str(e["num_oid"]), unit: unit, defaults: defaults,
extra: append([]string(nil), cnote...), handled: sensorHandled}
if class == "state" {
en.states = parseStates(e["states"])
}
im.addEntry(en)
}
}
}