added netbox api implementation

This commit is contained in:
ston1th 2021-10-10 15:47:23 +02:00
commit 1daa3f8e0e
3 changed files with 277 additions and 340 deletions

276
netbox/netbox.go Normal file
View file

@ -0,0 +1,276 @@
package netbox
import (
"git.giftfish.de/ston1th/netalloc"
"crypto/tls"
"encoding/json"
"errors"
"net"
"net/http"
"net/url"
"strconv"
"time"
)
type result struct {
ID int `json:"id"`
}
type resultList struct {
Results []result `json:"results"`
}
type ip struct {
Address string `json:"address"`
}
type detail struct {
Detail string `json:"detail"`
}
type Allocator struct {
httpClient *http.Client
token string
prefix string
endpoint string
insecure bool
prefixURL string
prefixNet *net.IPNet
}
type Option func(*Allocator)
func WithHTTPClient(c *http.Client) Option {
return func(a *Allocator) {
a.httpClient = c
}
}
func WithEndpoint(endpoint string) Option {
return func(a *Allocator) {
a.endpoint = endpoint
}
}
func WithToken(token string) Option {
return func(a *Allocator) {
a.token = "Token " + token
}
}
func WithInsecureClient(insecure bool) Option {
return func(a *Allocator) {
a.insecure = insecure
}
}
func WithPrefix(prefix string) Option {
return func(a *Allocator) {
a.prefix = prefix
}
}
var (
ErrMissingPrefix = errors.New("missing prefix config")
ErrMissingEndpoint = errors.New("missing endpoint config")
ErrMissingToken = errors.New("missing token config")
ErrPrefixNotFound = errors.New("prefix not found")
ErrIPNotFound = errors.New("ip not found")
ErrUnexpectedStatusCode = errors.New("unexpected status code")
)
func NewAllocator(opts ...Option) (netalloc.Allocator, error) {
a := &Allocator{}
for _, opt := range opts {
opt(a)
}
if a.httpClient == nil {
a.httpClient = &http.Client{Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
}).DialContext,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: a.insecure,
},
}}
}
if a.prefix == "" {
return nil, ErrMissingPrefix
}
if a.endpoint == "" {
return nil, ErrMissingEndpoint
}
if a.token == "" {
return nil, ErrMissingToken
}
_, prefixnet, err := net.ParseCIDR(a.prefix)
if err != nil {
return nil, err
}
a.prefixNet = prefixnet
v := url.Values{
"prefix": []string{a.prefix},
"limit": []string{"1"},
}
a.prefixURL = a.endpoint + "/api/ipam/prefixes/?" + v.Encode()
return a, nil
}
// SetStore is a no-op
func (a *Allocator) SetStore(_ netalloc.Store) {}
func (a *Allocator) prefixID() (id string, err error) {
req, err := http.NewRequest("GET", a.prefixURL, nil)
if err != nil {
return
}
req.Header.Set("Authorization", a.token)
resp, err := a.httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden {
var d detail
err = json.NewDecoder(resp.Body).Decode(&d)
if err != nil {
return
}
return "", errors.New(d.Detail)
}
var rl resultList
err = json.NewDecoder(resp.Body).Decode(&rl)
if err != nil {
return
}
if len(rl.Results) == 0 {
return "", ErrPrefixNotFound
}
return strconv.Itoa(rl.Results[0].ID), nil
}
func (a *Allocator) Alloc() (*net.IPAddr, error) {
cidr, err := a.AllocCIDR()
if err != nil {
return nil, err
}
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
return &net.IPAddr{IP: ip}, nil
}
func (a *Allocator) AllocCIDR() (cidr string, err error) {
id, err := a.prefixID()
if err != nil {
return
}
url := a.endpoint + "/api/ipam/prefixes/" + id + "/available-ips/"
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return
}
req.Header.Set("Authorization", a.token)
req.Header.Set("Content-Type", "application/json")
resp, err := a.httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return "", netalloc.ErrPrefixFull
}
if resp.StatusCode == http.StatusCreated {
var ip ip
err = json.NewDecoder(resp.Body).Decode(&ip)
if err != nil {
return
}
return ip.Address, nil
}
return "", ErrUnexpectedStatusCode
}
func (a *Allocator) ipID(cidr string) (id string, err error) {
v := url.Values{
"address": []string{cidr},
"limit": []string{"1"},
}
url := a.endpoint + "/api/ipam/ip-addresses/?" + v.Encode()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
req.Header.Set("Authorization", a.token)
resp, err := a.httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden {
var d detail
err = json.NewDecoder(resp.Body).Decode(&d)
if err != nil {
return
}
return "", errors.New(d.Detail)
}
var rl resultList
err = json.NewDecoder(resp.Body).Decode(&rl)
if err != nil {
return
}
if len(rl.Results) == 0 {
return "", ErrIPNotFound
}
return strconv.Itoa(rl.Results[0].ID), nil
}
func (a *Allocator) Free(ip *net.IPAddr) error {
return a.FreeCIDR(a.CIDR(ip))
}
func (a *Allocator) FreeCIDR(cidr string) (err error) {
id, err := a.ipID(cidr)
if err == ErrIPNotFound {
return nil
}
if err != nil {
return
}
url := a.endpoint + "/api/ipam/ip-addresses/" + id + "/"
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return
}
req.Header.Set("Authorization", a.token)
resp, err := a.httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden {
var d detail
err = json.NewDecoder(resp.Body).Decode(&d)
if err != nil {
return
}
return errors.New(d.Detail)
}
if resp.StatusCode == http.StatusNoContent ||
resp.StatusCode == http.StatusNotFound {
return nil
}
return ErrUnexpectedStatusCode
}
func (a *Allocator) CIDR(ip *net.IPAddr) string {
return netalloc.CIDR(a.prefixNet, ip)
}