added netbox api implementation
This commit is contained in:
parent
c39acaee43
commit
1daa3f8e0e
3 changed files with 277 additions and 340 deletions
276
netbox/netbox.go
Normal file
276
netbox/netbox.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue