package netbox import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/netip" "net/url" "git.faercol.me/faercol/dnsmasq-netbox-connector/internal/config" ) type NetboxClient struct { conf *config.NetboxConfig } type Device struct { ID int `json:"id"` Name string `json:"name"` } func (d Device) String() string { return fmt.Sprintf("Device %d (%s)", d.ID, d.Name) } type Interface struct { ID int `json:"id"` Name string `json:"name"` Device *Device `json:"device"` MacAddress string `json:"mac_address"` } func (i Interface) String() string { baseStr := fmt.Sprintf("Interface %d (%s - %s)", i.ID, i.Name, i.MacAddress) if i.Device == nil { return baseStr + " not associated to a device" } return baseStr + " associated device: " + i.Device.String() } type IP struct { ID int `json:"id"` Address netip.Prefix Interface *Interface `json:"assigned_object"` } func (i IP) String() string { baseStr := fmt.Sprintf("IP %d (%s)", i.ID, i.Address.String()) if i.Interface == nil { return baseStr + " unused" } return baseStr + " associated interface: " + i.Interface.String() } type ipsResult struct { Count int `json:"count"` Results []IP `json:"results"` } type interfacesResult struct { Count int `json:"count"` Results []*Interface `json:"results"` } func (c *NetboxClient) GetUsedDHCPAddresses(ctx context.Context) (used []*IP, free []*IP, err error) { clt := http.DefaultClient reqURL, err := url.JoinPath(c.conf.Host, "api/ipam/ip-addresses") if err != nil { return nil, nil, fmt.Errorf("failed to parse URL: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, nil, fmt.Errorf("failed to prepare request: %w", err) } q := req.URL.Query() q.Add("vrf_name", c.conf.VRFName) q.Add("status", "dhcp") // q.Add("assigned_object_id__empty", "False") req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Token "+c.conf.APIKey) resp, err := clt.Do(req) if err != nil { return nil, nil, fmt.Errorf("failed to run query: %w", err) } if resp.StatusCode != http.StatusOK { return nil, nil, fmt.Errorf("unexpected returncode: %d", resp.StatusCode) } respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, nil, fmt.Errorf("failed to read response body: %w", err) } var res ipsResult if err := json.Unmarshal(respBody, &res); err != nil { return nil, nil, fmt.Errorf("failed to parse response: %w", err) } for _, r := range res.Results { if r.Interface == nil { free = append(free, &r) } else { used = append(used, &r) } } return } func (c *NetboxClient) GetAllInterfaces(ctx context.Context) ([]*Interface, error) { clt := http.DefaultClient reqURL, err := url.JoinPath(c.conf.Host, "api/dcim/interfaces") if err != nil { return nil, fmt.Errorf("failed to parse URL: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, fmt.Errorf("failed to prepare request: %w", err) } q := req.URL.Query() req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Token "+c.conf.APIKey) resp, err := clt.Do(req) if err != nil { return nil, fmt.Errorf("failed to run query: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected returncode: %d", resp.StatusCode) } respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var res interfacesResult if err := json.Unmarshal(respBody, &res); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return res.Results, nil } func (c *NetboxClient) UpdateIPs(ctx context.Context, unassignedIPs []*IP, updatedIPs []*IP) error { clt := http.DefaultClient reqURL, err := url.JoinPath(c.conf.Host, "api/ipam/ip-addresses/") if err != nil { return fmt.Errorf("failed to parse URL: %w", err) } queryData := []map[string]interface{}{} for _, i := range unassignedIPs { ifaceData := map[string]interface{}{ "id": i.ID, "assigned_object_type": nil, "assigned_object_id": nil, } queryData = append(queryData, ifaceData) } for _, i := range updatedIPs { ifaceData := map[string]interface{}{ "id": i.ID, "assigned_object_type": "dcim.interface", "assigned_object_id": i.Interface.ID, } queryData = append(queryData, ifaceData) } queryBody, err := json.Marshal(queryData) if err != nil { return fmt.Errorf("failed to serialize body: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPatch, reqURL, bytes.NewBuffer(queryBody)) if err != nil { return fmt.Errorf("failed to prepare request: %w", err) } q := req.URL.Query() req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Token "+c.conf.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := clt.Do(req) if err != nil { return fmt.Errorf("failed to run query: %w", err) } respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected returncode: %d (%s)", resp.StatusCode, string(respBody)) } return nil } func New(conf config.Config) *NetboxClient { return &NetboxClient{ conf: &conf.NetboxConfig, } }