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, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, 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) }