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
173 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|