Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package importcsv
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// Result counts what Apply wrote.
|
|
type Result struct {
|
|
DevicesCreated int
|
|
CredentialsCreated int
|
|
PortsUpdated int
|
|
// DeviceIDs are the ids of the devices created, in CSV order.
|
|
DeviceIDs []int64
|
|
}
|
|
|
|
// Apply writes the plan in one transaction: new credentials, devices with
|
|
// addresses, parent links, then port aliases. Any error rolls everything back.
|
|
func Apply(ctx context.Context, st store.Store, p *Plan) (Result, error) {
|
|
var res Result
|
|
err := st.WithTx(ctx, func(tx store.Store) error {
|
|
res = Result{}
|
|
ids, err := createDevices(ctx, tx, p.Devices, &res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := linkParents(ctx, tx, p.Devices, ids); err != nil {
|
|
return err
|
|
}
|
|
return applyPorts(ctx, tx, p.Ports, &res)
|
|
})
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("import: %w", err)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// createDevices returns the new device ids by planned name.
|
|
func createDevices(ctx context.Context, tx store.Store, plans []DevicePlan, res *Result) (map[string]int64, error) {
|
|
ids := map[string]int64{}
|
|
for _, dp := range plans {
|
|
d := &store.Device{
|
|
Name: dp.Name, Kind: dp.Kind, Site: dp.Site, Role: dp.Role, Enabled: true, SNMPPort: dp.Port,
|
|
}
|
|
cid, err := credentialFor(ctx, tx, dp, res)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("row %d: %w", dp.Row, err)
|
|
}
|
|
if cid != 0 {
|
|
d.SNMPCredentialID = &cid
|
|
}
|
|
if err := tx.CreateDevice(ctx, d); err != nil {
|
|
return nil, fmt.Errorf("row %d: create device %q: %w", dp.Row, dp.Name, err)
|
|
}
|
|
if len(dp.Addresses) > 0 {
|
|
if err := tx.SetAddresses(ctx, d.ID, dp.Addresses); err != nil {
|
|
return nil, fmt.Errorf("row %d: addresses of %q: %w", dp.Row, dp.Name, err)
|
|
}
|
|
}
|
|
ids[dp.Name] = d.ID
|
|
res.DeviceIDs = append(res.DeviceIDs, d.ID)
|
|
res.DevicesCreated++
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func credentialFor(ctx context.Context, tx store.Store, dp DevicePlan, res *Result) (int64, error) {
|
|
if dp.NewCredential == nil {
|
|
return dp.CredentialID, nil
|
|
}
|
|
c := *dp.NewCredential
|
|
if err := tx.CreateCredential(ctx, &c); err != nil {
|
|
return 0, fmt.Errorf("create credential %q: %w", c.Name, err)
|
|
}
|
|
res.CredentialsCreated++
|
|
return c.ID, nil
|
|
}
|
|
|
|
func linkParents(ctx context.Context, tx store.Store, plans []DevicePlan, ids map[string]int64) error {
|
|
for _, dp := range plans {
|
|
if len(dp.Parents) == 0 {
|
|
continue
|
|
}
|
|
pids := make([]int64, 0, len(dp.Parents))
|
|
for _, key := range dp.Parents {
|
|
id, ok := ids[key]
|
|
if !ok {
|
|
d, err := tx.GetDeviceBySlug(ctx, key)
|
|
if err != nil {
|
|
return fmt.Errorf("row %d: parent %q: %w", dp.Row, key, err)
|
|
}
|
|
id = d.ID
|
|
}
|
|
pids = append(pids, id)
|
|
}
|
|
if err := tx.SetParents(ctx, ids[dp.Name], store.RelNetwork, pids); err != nil {
|
|
return fmt.Errorf("row %d: parents of %q: %w", dp.Row, dp.Name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyPorts(ctx context.Context, tx store.Store, ports []PortPlan, res *Result) error {
|
|
byDev := map[int64][]store.Interface{}
|
|
for _, pp := range ports {
|
|
if _, ok := byDev[pp.DeviceID]; !ok {
|
|
ifs, err := tx.ListInterfaces(ctx, pp.DeviceID, false)
|
|
if err != nil {
|
|
return fmt.Errorf("row %d: %w", pp.Row, err)
|
|
}
|
|
byDev[pp.DeviceID] = ifs
|
|
}
|
|
ifs := byDev[pp.DeviceID]
|
|
for i := range ifs {
|
|
if ifs[i].ID == pp.IfaceID {
|
|
ifs[i].Description = pp.Description
|
|
if err := tx.UpsertInterfaces(ctx, pp.DeviceID, []store.Interface{ifs[i]}); err != nil {
|
|
return fmt.Errorf("row %d: set description on %s: %w", pp.Row, pp.IfName, err)
|
|
}
|
|
res.PortsUpdated++
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|