Add link to netbox
This commit is contained in:
parent
a484431d13
commit
9e5e0a51fb
3 changed files with 305 additions and 0 deletions
|
@ -20,10 +20,17 @@ type DNSMasqConfig struct {
|
|||
LeasesPath string
|
||||
}
|
||||
|
||||
type NetboxConfig struct {
|
||||
Host string `json:"host"`
|
||||
APIKey string `json:"api_key"`
|
||||
VRFName string `json:"vrf"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
HealthchecksConfig `json:"healthchecks"`
|
||||
DNSMasqConfigFile string `json:"dnsmasq_config"`
|
||||
DNSMasqConfig
|
||||
NetboxConfig `json:"netbox"`
|
||||
}
|
||||
|
||||
func (c *Config) parseDNSMasqConf() error {
|
||||
|
|
213
internal/netbox/netbox.go
Normal file
213
internal/netbox/netbox.go
Normal file
|
@ -0,0 +1,213 @@
|
|||
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,
|
||||
}
|
||||
}
|
85
main.go
85
main.go
|
@ -4,10 +4,12 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.faercol.me/faercol/dnsmasq-netbox-connector/internal/config"
|
||||
"git.faercol.me/faercol/dnsmasq-netbox-connector/internal/dnsmasq"
|
||||
"git.faercol.me/faercol/dnsmasq-netbox-connector/internal/healthchecks"
|
||||
"git.faercol.me/faercol/dnsmasq-netbox-connector/internal/netbox"
|
||||
gohealthchecks "git.faercol.me/faercol/go-healthchecks"
|
||||
)
|
||||
|
||||
|
@ -44,7 +46,90 @@ func main() {
|
|||
fmt.Printf("Got lease %s\n", l)
|
||||
}
|
||||
|
||||
netboxClt := netbox.New(*conf)
|
||||
assignedIP, freeIP, err := netboxClt.GetUsedDHCPAddresses(context.Background())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to get list of assigned IPs: %s\n", err)
|
||||
if err := healthchecks.Failure(context.Background(), conf.HealthchecksConfig, healtchecksClt, check, "failed to get assigned IPs"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to notify failure: %s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
allInterfaces, err := netboxClt.GetAllInterfaces(context.Background())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to get list of interfaces: %s\n", err)
|
||||
if err := healthchecks.Failure(context.Background(), conf.HealthchecksConfig, healtchecksClt, check, "failed to get interfaces"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to notify failure: %s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
toDelete, toAdd := reconcile(leases, assignedIP, freeIP, allInterfaces)
|
||||
for _, a := range toDelete {
|
||||
fmt.Printf("Following IP should be unassigned: %s\n", a.String())
|
||||
}
|
||||
for _, a := range toAdd {
|
||||
fmt.Printf("Following IP should be assigned: %s\n", a.String())
|
||||
}
|
||||
|
||||
if err := netboxClt.UpdateIPs(context.Background(), toDelete, toAdd); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to update IP assignment in netbox: %s\n", err)
|
||||
if err := healthchecks.Failure(context.Background(), conf.HealthchecksConfig, healtchecksClt, check, "failed to update assignment"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to notify failure: %s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := healthchecks.Success(context.Background(), conf.HealthchecksConfig, healtchecksClt, check); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to notify process success: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// this method would probably require a bit of refacto, it's quite ugly
|
||||
func reconcile(leases []*dnsmasq.Lease, assigned []*netbox.IP, free []*netbox.IP, interfaces []*netbox.Interface) (toDelete []*netbox.IP, toAdd []*netbox.IP) {
|
||||
dhcpMapping := map[string]string{}
|
||||
for _, l := range leases {
|
||||
dhcpMapping[l.IP.String()] = l.Mac
|
||||
}
|
||||
|
||||
assignedMapping := map[string]string{}
|
||||
freeMapping := map[string]*netbox.IP{}
|
||||
|
||||
interfaceMapping := map[string]*netbox.Interface{}
|
||||
for _, i := range interfaces {
|
||||
if i.MacAddress == "" {
|
||||
continue
|
||||
}
|
||||
interfaceMapping[strings.ToLower(i.MacAddress)] = i
|
||||
}
|
||||
|
||||
for _, a := range assigned {
|
||||
assignedMapping[a.Address.Addr().String()] = ""
|
||||
_, ok := dhcpMapping[a.Address.Addr().String()]
|
||||
if !ok {
|
||||
toDelete = append(toDelete, a)
|
||||
}
|
||||
}
|
||||
for _, a := range free {
|
||||
freeMapping[a.Address.Addr().String()] = a
|
||||
}
|
||||
|
||||
for _, l := range leases {
|
||||
_, ok := assignedMapping[l.IP.String()]
|
||||
if !ok {
|
||||
iface, ok := interfaceMapping[strings.ToLower(l.Mac)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip, ok := freeMapping[l.IP.String()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip.Interface = iface
|
||||
toAdd = append(toAdd, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue