Files
nmon/internal/librenms/detect.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

115 lines
3.0 KiB
Go

package librenms
import (
"fmt"
"regexp"
"strings"
)
var numericOIDRE = regexp.MustCompile(`^\.?[0-9]+(\.[0-9]+)+$`)
// match is the profile match: section built from an os_detection file.
type match struct {
sysObjectID []string
sysDescr []string
notes []string // conditions that could not be carried over
}
// phpRegex converts a PHP /pattern/flags regular expression to a Go one.
func phpRegex(s string) (string, error) {
s = strings.TrimSpace(s)
if len(s) < 2 {
return "", fmt.Errorf("not delimited")
}
d := s[0]
end := strings.LastIndexByte(s, d)
if end <= 0 {
return "", fmt.Errorf("no closing delimiter")
}
pat, flags := s[1:end], s[end+1:]
prefix := ""
for _, f := range flags {
switch f {
case 'i', 's', 'm':
prefix += string(f)
case 'u', 'D', 'U':
default:
return "", fmt.Errorf("unsupported flag %q", string(f))
}
}
if prefix != "" {
pat = "(?" + prefix + ")" + pat
}
if _, err := regexp.Compile(pat); err != nil {
return "", err
}
return pat, nil
}
// parseDetection maps the discovery: list of an os_detection file.
func parseDetection(doc map[string]any) match {
var m match
for gi, g := range asList(doc["discovery"]) {
e := asMap(g)
if e == nil {
m.notes = append(m.notes, fmt.Sprintf("discovery[%d]: not a mapping, skipped", gi))
continue
}
oids, descr := parseDetectionGroup(&m, gi, e)
if len(oids) > 0 && len(descr) > 0 {
m.notes = append(m.notes, fmt.Sprintf("discovery[%d] requires sysObjectID AND sysDescr; "+
"only sysObjectID is used, so this match is broader than LibreNMS's", gi))
descr = nil
}
m.sysObjectID = appendNew(m.sysObjectID, oids...)
m.sysDescr = appendNew(m.sysDescr, descr...)
}
return m
}
// parseDetectionGroup reads one discovery entry, recording what it cannot express in m.notes.
func parseDetectionGroup(m *match, gi int, e map[string]any) (oids, descr []string) {
for _, k := range sortedKeys(e) {
switch k {
case "sysObjectID":
for _, o := range strs(e[k]) {
if !numericOIDRE.MatchString(strings.TrimSpace(o)) {
m.notes = append(m.notes, fmt.Sprintf("discovery[%d].sysObjectID %q is not a numeric OID, skipped", gi, o))
continue
}
oids = append(oids, strings.TrimPrefix(strings.TrimSpace(o), "."))
}
case "sysDescr":
for _, d := range strs(e[k]) {
descr = append(descr, regexp.QuoteMeta(d))
}
case "sysDescr_regex":
for _, d := range strs(e[k]) {
re, err := phpRegex(d)
if err != nil {
m.notes = append(m.notes, fmt.Sprintf("discovery[%d].sysDescr_regex %q skipped: %v", gi, oneLine(d), err))
continue
}
descr = append(descr, re)
}
default:
m.notes = append(m.notes, fmt.Sprintf("discovery[%d] has an extra condition %q that profile match cannot express "+
"(LibreNMS requires it in addition)", gi, k))
}
}
return oids, descr
}
func appendNew(dst []string, v ...string) []string {
outer:
for _, x := range v {
for _, y := range dst {
if x == y {
continue outer
}
}
dst = append(dst, x)
}
return dst
}