refactor: phase 2 structural alignment — reports, importer, nodecli, pagination
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
// Result of importing module entries from CSV.
|
||||
type Result struct {
|
||||
Imported int
|
||||
ModuleType string
|
||||
}
|
||||
|
||||
// ImportModuleEntriesCSV parses and creates entries for AS_PREFIXES, DOMAINS, or IP_RANGES modules.
|
||||
func ImportModuleEntriesCSV(st store.Backend, tenantID, moduleID string, body io.Reader) (*Result, error) {
|
||||
mod, err := st.GetModule(tenantID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cr := csv.NewReader(io.LimitReader(body, 8<<20))
|
||||
cr.TrimLeadingSpace = true
|
||||
cr.FieldsPerRecord = -1
|
||||
rows, err := cr.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("importer: invalid csv: %w", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil, fmt.Errorf("importer: %w", store.ErrInvalidInput)
|
||||
}
|
||||
|
||||
communities, err := st.ListCommunities(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
communityIDByID := make(map[string]string, len(communities))
|
||||
communityIDByValue := make(map[string]string, len(communities))
|
||||
for _, c := range communities {
|
||||
communityIDByID[c.ID] = c.ID
|
||||
communityIDByValue[strings.TrimSpace(c.Community)] = c.ID
|
||||
}
|
||||
|
||||
resolveCommunity := func(raw string, required bool) (*string, error) {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" {
|
||||
if required {
|
||||
return nil, fmt.Errorf("community is required")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if id, ok := communityIDByID[v]; ok {
|
||||
return &id, nil
|
||||
}
|
||||
if id, ok := communityIDByValue[v]; ok {
|
||||
return &id, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown community %q", v)
|
||||
}
|
||||
|
||||
start := 0
|
||||
if len(rows[0]) >= 2 {
|
||||
key := strings.ToLower(strings.TrimSpace(rows[0][0]))
|
||||
switch key {
|
||||
case "asn", "domain", "iprange":
|
||||
start = 1
|
||||
}
|
||||
}
|
||||
|
||||
imported := 0
|
||||
switch mod.Type {
|
||||
case "AS_PREFIXES":
|
||||
for i := start; i < len(rows); i++ {
|
||||
rec := rows[i]
|
||||
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
||||
continue
|
||||
}
|
||||
if len(rec) < 2 {
|
||||
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
|
||||
}
|
||||
asn, err := strconv.ParseInt(strings.TrimSpace(rec[0]), 10, 64)
|
||||
if err != nil || asn <= 0 {
|
||||
return nil, fmt.Errorf("importer: line %d: invalid asn", i+1)
|
||||
}
|
||||
cid, err := resolveCommunity(rec[1], false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
|
||||
}
|
||||
if _, err := st.CreateASEntry(tenantID, moduleID, &store.ASEntry{ASN: asn, CommunityID: cid}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imported++
|
||||
}
|
||||
case "DOMAINS":
|
||||
for i := start; i < len(rows); i++ {
|
||||
rec := rows[i]
|
||||
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
||||
continue
|
||||
}
|
||||
if len(rec) < 2 {
|
||||
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
|
||||
}
|
||||
fqdn := strings.TrimSpace(rec[0])
|
||||
if fqdn == "" {
|
||||
return nil, fmt.Errorf("importer: line %d: domain is required", i+1)
|
||||
}
|
||||
cid, err := resolveCommunity(rec[1], false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
|
||||
}
|
||||
if _, err := st.CreateDomainEntry(tenantID, moduleID, &store.DomainEntry{FQDN: fqdn, CommunityID: cid}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imported++
|
||||
}
|
||||
case "IP_RANGES":
|
||||
for i := start; i < len(rows); i++ {
|
||||
rec := rows[i]
|
||||
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
||||
continue
|
||||
}
|
||||
if len(rec) < 2 {
|
||||
return nil, fmt.Errorf("importer: line %d: expected 2 columns", i+1)
|
||||
}
|
||||
prefix := strings.TrimSpace(rec[0])
|
||||
if prefix == "" {
|
||||
return nil, fmt.Errorf("importer: line %d: ipRange is required", i+1)
|
||||
}
|
||||
cid, err := resolveCommunity(rec[1], true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("importer: line %d: %w", i+1, err)
|
||||
}
|
||||
if _, err := st.CreateIPRangeEntry(tenantID, moduleID, &store.IPRangeEntry{Prefix: prefix, CommunityID: cid}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imported++
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("importer: csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
|
||||
}
|
||||
|
||||
return &Result{Imported: imported, ModuleType: mod.Type}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user