Files
nmon/internal/importcsv/devices.go

163 lines
5.0 KiB
Go

package importcsv
import (
"fmt"
"io"
"net/netip"
"strconv"
"strings"
"github.com/v10networks/nmon/internal/store"
)
// Device CSV columns: name (required), addresses (";"-separated), site, role, kind (default network),
// snmp_version, community, credential (name of an existing credential), port, parents (";"-separated
// slugs or names of existing devices or of other rows in the same file).
func parseDevices(r io.Reader, inv *inventory) ([]DevicePlan, []RowError, error) {
cols, rows, err := readRecords(r, FileDevices, "name")
if err != nil {
return nil, nil, err
}
var plans []DevicePlan
var errs []RowError
seen := map[string]int{}
for i, rec := range rows {
if blank(rec) {
continue
}
row := i + 2
p, msgs := parseDeviceRow(cols, rec, row, inv, seen)
for _, m := range msgs {
errs = append(errs, RowError{File: FileDevices, Row: row, Msg: m})
}
plans = append(plans, p)
}
errs = append(errs, resolveParents(plans, inv)...)
return plans, errs, nil
}
func parseDeviceRow(cols map[string]int, rec []string, row int, inv *inventory, seen map[string]int) (DevicePlan, []string) {
var msgs []string
p := DevicePlan{
Row: row, Name: field(cols, rec, "name"), Site: field(cols, rec, "site"), Role: field(cols, rec, "role"),
Kind: strings.ToLower(field(cols, rec, "kind")), Port: store.DefaultSNMPPort,
}
if p.Name == "" {
msgs = append(msgs, "name is required")
} else {
msgs = append(msgs, checkName(p.Name, row, inv, seen)...)
}
if p.Kind == "" {
p.Kind = store.KindNetwork
}
switch p.Kind {
case store.KindNetwork, store.KindVM, store.KindContainer, store.KindCheckOnly:
default:
msgs = append(msgs, fmt.Sprintf("kind %q must be network, vm, container or check_only", p.Kind))
}
addrs, m := parseAddresses(field(cols, rec, "addresses"))
p.Addresses, msgs = addrs, append(msgs, m...)
if s := field(cols, rec, "port"); s != "" {
n, err := strconv.Atoi(s)
if err != nil || n < 1 || n > 65535 {
msgs = append(msgs, fmt.Sprintf("port %q must be 1-65535", s))
} else {
p.Port = n
}
}
p.CredentialID, p.NewCredential, m = parseCredential(cols, rec, p.Name, inv)
msgs = append(msgs, m...)
p.Parents = splitList(field(cols, rec, "parents"))
return p, msgs
}
func checkName(name string, row int, inv *inventory, seen map[string]int) []string {
k := strings.ToLower(name)
var msgs []string
if prev, ok := seen[k]; ok {
msgs = append(msgs, fmt.Sprintf("duplicate device name %q (first on row %d)", name, prev))
}
seen[k] = row
if _, ok := inv.lookup(name); ok {
msgs = append(msgs, fmt.Sprintf("device %q already exists", name))
}
return msgs
}
func parseAddresses(s string) ([]string, []string) {
var out, msgs []string
for _, a := range splitList(s) {
ip, err := netip.ParseAddr(a)
if err != nil {
msgs = append(msgs, fmt.Sprintf("address %q is not an IP address", a))
continue
}
out = append(out, ip.String())
}
return out, msgs
}
// parseCredential resolves a named credential, or builds a new v1/v2c one from an inline community.
func parseCredential(cols map[string]int, rec []string, devName string, inv *inventory) (int64, *store.SNMPCredential, []string) {
name, ver, comm := field(cols, rec, "credential"), strings.ToLower(field(cols, rec, "snmp_version")), field(cols, rec, "community")
switch {
case name != "":
id, ok := inv.creds[strings.ToLower(name)]
if !ok {
return 0, nil, []string{fmt.Sprintf("credential %q does not exist", name)}
}
return id, nil, nil
case comm != "":
if ver == "" {
ver = "v2c"
}
if ver != "v1" && ver != "v2c" {
return 0, nil, []string{fmt.Sprintf("snmp_version %q with a community must be v1 or v2c", ver)}
}
cname := "import-" + devName
if _, taken := inv.creds[strings.ToLower(cname)]; taken {
return 0, nil, []string{fmt.Sprintf("credential %q already exists; name it in the credential column", cname)}
}
return 0, &store.SNMPCredential{Name: cname, Version: ver, Community: comm}, nil
case ver == "v3":
return 0, nil, []string{"v3 devices must name an existing credential"}
}
return 0, nil, nil
}
// resolveParents rewrites each parent token to its key: the slug of an existing device or the
// name of another planned device. Unknown parents and self-parents are row errors.
func resolveParents(plans []DevicePlan, inv *inventory) []RowError {
planned := map[string]string{} // lower-case name to name
for _, p := range plans {
planned[strings.ToLower(p.Name)] = p.Name
}
var errs []RowError
for i := range plans {
p := &plans[i]
keys := make([]string, 0, len(p.Parents))
for _, tok := range p.Parents {
if d, ok := inv.lookup(tok); ok {
keys = append(keys, d.Slug)
} else if n, ok := planned[strings.ToLower(tok)]; ok {
keys = append(keys, n)
} else {
errs = append(errs, RowError{File: FileDevices, Row: p.Row, Msg: fmt.Sprintf("parent %q is not a known device", tok)})
}
}
p.Parents = keys
}
return errs
}
func splitList(s string) []string {
var out []string
for _, f := range strings.Split(s, ";") {
if f = strings.TrimSpace(f); f != "" {
out = append(out, f)
}
}
return out
}