276 lines
5.5 KiB
Go
276 lines
5.5 KiB
Go
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)
|
|
}
|