Files
ssh-netbox-wrapper/internal/netbox/sources.go
2026-10-27 23:14:42 +01:00

114 lines
3.0 KiB
Go

package netbox
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
)
// Source binds a name to its own API client and credentials.
type Source struct {
Name string
URL string
Token string
TokenVersion int
}
func NewMultiClient(sources []Source) *Client {
c := &Client{sources: make(map[string]*Client)}
for _, source := range sources {
c.sources[strings.ToLower(source.Name)] = NewClient(source.URL, source.Token, source.TokenVersion)
}
return c
}
func (c *Client) searchSources(ctx context.Context, query string, opts SearchOptions, all bool) ([]HostEntry, error) {
requested, query := SplitTarget(query)
if requested != "" {
if _, ok := c.sources[requested]; !ok {
return nil, fmt.Errorf("unknown NetBox instance %q", requested)
}
}
var mu sync.Mutex
var wg sync.WaitGroup
var entries []HostEntry
var errs []error
started := 0
for name, client := range c.sources {
if requested != "" && name != requested {
continue
}
started++
wg.Add(1)
go func(name string, client *Client) {
defer wg.Done()
var found []HostEntry
var err error
if all {
found, err = client.SearchAll(ctx, query, opts)
} else {
found, err = client.Search(ctx, query, opts)
}
mu.Lock()
defer mu.Unlock()
if err != nil {
errs = append(errs, fmt.Errorf("NetBox %s: %w", name, err))
return
}
for i := range found {
found[i].Source = name
}
entries = append(entries, found...)
}(name, client)
}
wg.Wait()
// Full lookups must see every source to rule out ambiguous hostnames.
if len(errs) > 0 && (all || len(errs) == started) {
return nil, errors.Join(errs...)
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Target() < entries[j].Target() })
return entries, errors.Join(errs...)
}
func (c *Client) sourceForHost(host HostEntry) (*Client, HostEntry, error) {
prefix, name := SplitTarget(host.Name)
if prefix != "" {
if host.Source != "" && !strings.EqualFold(prefix, host.Source) {
return nil, host, fmt.Errorf("conflicting NetBox instance for %q", host.Name)
}
host.Source, host.Name = prefix, name
}
if c.sources == nil {
return c, host, nil
}
source, ok := c.sources[strings.ToLower(host.Source)]
if !ok {
return nil, host, fmt.Errorf("specify a configured NetBox instance as instance/hostname (got %q)", host.Target())
}
host.Source = strings.ToLower(host.Source)
return source, host, nil
}
// HostScope identifies a local note by its original NetBox URL and canonical
// hostname, so renaming an instance does not move or mix its notes.
func (c *Client) HostScope(host HostEntry) (HostEntry, string, error) {
source, host, err := c.sourceForHost(host)
if err != nil {
return host, "", err
}
return host, source.baseURL, nil
}
func (c *Client) GetIPsWithFilterForHost(ctx context.Context, host HostEntry, filters string) ([]string, error) {
source, _, err := c.sourceForHost(host)
if err != nil {
return nil, err
}
return source.GetIPsWithFilter(ctx, filters)
}
func (c *Client) MultipleSources() bool { return c.sources != nil }