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 }