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 }