214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
|
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,
|
||
|
}
|
||
|
}
|