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
115 lines
3.0 KiB
Go
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
|
|
}
|