From 02f5fbcdd06c50d13c4f4df09fc29ca479dd492b Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Wed, 27 Sep 2023 16:55:23 -0700 Subject: netbox: refactor out HTTP client --- clients/netbox/client.go | 70 +++------------ clients/netbox/go.mod | 4 +- clients/netbox/gql_dns_servers.go | 47 ++++++++++ clients/netbox/http.go | 174 ++++++++++++++++++++++++++++++++++++++ clients/netbox/model.go | 7 +- 5 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 clients/netbox/gql_dns_servers.go create mode 100644 clients/netbox/http.go diff --git a/clients/netbox/client.go b/clients/netbox/client.go index ad2967a..3b7fbaf 100644 --- a/clients/netbox/client.go +++ b/clients/netbox/client.go @@ -2,12 +2,8 @@ package netbox import ( "context" - "encoding/json" "fmt" - "io" "net" - "net/http" - "net/url" "strconv" ) @@ -18,49 +14,11 @@ type NetboxClient interface { } type BasicNetboxClient struct { - ApiKey string - Endpoint string + *NetboxHttpClient } -var _ NetboxClient = (*BasicNetboxClient)(nil) - -func (c *BasicNetboxClient) makeRequestRaw(ctx context.Context, method, u string, ib io.Reader, o interface{}) error { - req, err := http.NewRequestWithContext(ctx, method, u, ib) - if err != nil { - return err - } - req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.ApiKey)) - - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - apiError := &ApiError{Status: res.StatusCode} - if err = json.NewDecoder(res.Body).Decode(apiError); err != nil { - return fmt.Errorf("Netbox JSON decode error while parsing error with status %d: %w", res.StatusCode, err) - } - return apiError - } - - if err = json.NewDecoder(res.Body).Decode(o); err != nil { - return err - } - - return nil -} - -func (c *BasicNetboxClient) makeRequest(ctx context.Context, method, path string, q url.Values, ib io.Reader, o interface{}) error { - u, err := url.Parse(c.Endpoint) - if err != nil { - return err - } - u.Path = path - u.RawQuery = q.Encode() - - return c.makeRequestRaw(ctx, method, u.String(), ib, o) +func MustNewBasicNetboxClient(apiKey, endpoint string) NetboxClient { + return &BasicNetboxClient{MustNewNetboxHttpClient(apiKey, endpoint)} } func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site string, tag string) ([]*net.IPNet, error) { @@ -71,12 +29,12 @@ func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site str out := []*net.IPNet{} - q := url.Values{} + q := NewNetboxGetRequest("/api/ipam/prefixes/") q.Add("site_id", strconv.Itoa(s)) q.Add("tag", tag) page := &PrefixList{} - if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil { + if err := c.Do(ctx, q, page); err != nil { return nil, err } @@ -90,7 +48,7 @@ func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site str for page.Next != "" { page = &PrefixList{} - if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { + if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { return nil, err } @@ -109,11 +67,11 @@ func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site str func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) ([]*net.IPNet, error) { out := []*net.IPNet{} - q := url.Values{} + q := NewNetboxGetRequest("/api/ipam/prefixes/") q.Add("tag", tag) page := &PrefixList{} - if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil { + if err := c.Do(ctx, q, page); err != nil { return nil, err } @@ -127,7 +85,7 @@ func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) for page.Next != "" { page = &PrefixList{} - if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { + if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { return nil, err } @@ -144,11 +102,11 @@ func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) } func (c *BasicNetboxClient) resolveSiteNameToId(ctx context.Context, s string) (int, error) { - q := url.Values{} + q := NewNetboxGetRequest("/api/dcim/sites/") q.Add("name", s) out := &SiteList{} - if err := c.makeRequest(ctx, http.MethodGet, "/api/dcim/sites/", q, nil, out); err != nil { + if err := c.Do(ctx, q, out); err != nil { return 0, err } @@ -162,11 +120,11 @@ func (c *BasicNetboxClient) resolveSiteNameToId(ctx context.Context, s string) ( func (c *BasicNetboxClient) GetServicesForVm(ctx context.Context, vmName string) ([]*Service, error) { out := []*Service{} - q := url.Values{} + q := NewNetboxGetRequest("/api/ipam/services/") q.Add("virtual_machine", vmName) page := &ServiceList{} - if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/services/", q, nil, page); err != nil { + if err := c.Do(ctx, q, page); err != nil { return nil, err } @@ -176,7 +134,7 @@ func (c *BasicNetboxClient) GetServicesForVm(ctx context.Context, vmName string) for page.Next != "" { page = &ServiceList{} - if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { + if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { return nil, err } diff --git a/clients/netbox/go.mod b/clients/netbox/go.mod index 1a3ea97..8a66bed 100644 --- a/clients/netbox/go.mod +++ b/clients/netbox/go.mod @@ -1,6 +1,6 @@ -module code.crute.us/mcrute/golib/clients/netbox/v3 +module code.crute.us/mcrute/golib/clients/netbox/v4 -go 1.18 +go 1.20 require ( github.com/mitchellh/mapstructure v1.5.0 diff --git a/clients/netbox/gql_dns_servers.go b/clients/netbox/gql_dns_servers.go new file mode 100644 index 0000000..67b4ad6 --- /dev/null +++ b/clients/netbox/gql_dns_servers.go @@ -0,0 +1,47 @@ +package netbox + +const dnsServerGQLQuery = `fragment VMHostDetails on VMInterfaceType{ + virtual_machine { + site { + name + } + } +} + +fragment HostDetails on InterfaceType { + device { + site { + name + } + } +} + +query { + ip_address_list(tag:"dns-server") { + address + assigned_object{ + ...VMHostDetails + ...HostDetails + } + } +}` + +type dnsServerGQLResponse struct { + Data struct { + AddressList []struct { + Address string `json:"address"` + AssignedObject struct { + VirtualMachine struct { + Site struct { + Name string `json:"name"` + } `json:"site"` + } `json:"virtual_machine"` + Device struct { + Site struct { + Name string `json:"name"` + } `json:"site"` + } `json:"device"` + } `json:"assigned_object"` + } `json:"ip_address_list"` + } `json:"data"` +} diff --git a/clients/netbox/http.go b/clients/netbox/http.go new file mode 100644 index 0000000..21a2b4b --- /dev/null +++ b/clients/netbox/http.go @@ -0,0 +1,174 @@ +package netbox + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// NetboxHttpClient is an HTTP client for the Netbox API. It is very +// low-level and should not be consumed by most clients. Instead use a +// client implementing NetboxClient. +type NetboxHttpClient struct { + ApiKey string + Endpoint url.URL +} + +func MustNewNetboxHttpClient(apiKey, endpoint string) *NetboxHttpClient { + u, err := url.Parse(endpoint) + if err != nil { + panic(err) + } + return &NetboxHttpClient{apiKey, *u} +} + +func (c *NetboxHttpClient) Do(ctx context.Context, r NetboxRequest, out any) error { + u := c.Endpoint // Store a local copy, requests may mutate it + req, err := r.BuildRequest(ctx, &u) + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.ApiKey)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + apiError := &ApiError{Status: res.StatusCode} + if err = json.NewDecoder(res.Body).Decode(apiError); err != nil { + return fmt.Errorf("Netbox JSON decode error while parsing error with status %d: %w", res.StatusCode, err) + } + return apiError + } + + if out != nil { + return json.NewDecoder(res.Body).Decode(out) + } + + return nil +} + +type NetboxRequest interface { + BuildRequest(context.Context, *url.URL) (*http.Request, error) +} + +type NetboxGraphQLRequest struct { + Query string +} + +var _ NetboxRequest = (*NetboxGraphQLRequest)(nil) + +func (r *NetboxGraphQLRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { + q, err := json.Marshal(map[string]string{"query": r.Query}) + if err != nil { + return nil, err + } + + host.Path = "/graphql/" + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + host.String(), + bytes.NewBuffer(q), + ) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + return req, nil +} + +type NetboxGetRequest struct { + url.Values + Path string +} + +var _ NetboxRequest = (*NetboxGetRequest)(nil) + +func NewNetboxGetRequest(path string) *NetboxGetRequest { + return &NetboxGetRequest{url.Values{}, path} +} + +func (r *NetboxGetRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { + host.Path = r.Path + host.RawQuery = r.Encode() + + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + host.String(), + nil, + ) + if err != nil { + return nil, err + } + + req.Header.Add("Accept", "application/json") + + return req, nil +} + +type NetboxRawGet struct { + Url string +} + +var _ NetboxRequest = (*NetboxRawGet)(nil) + +func (r *NetboxRawGet) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + r.Url, + nil, + ) + if err != nil { + return nil, err + } + + req.Header.Add("Accept", "application/json") + + return req, nil +} + +type NetboxJsonRequest struct { + Path string + Method string + Body any +} + +var _ NetboxRequest = (*NetboxJsonRequest)(nil) + +func (r *NetboxJsonRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { + host.Path = r.Path + + body, err := json.Marshal(r.Body) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext( + ctx, + r.Method, + host.String(), + bytes.NewBuffer(body), + ) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + return req, nil +} diff --git a/clients/netbox/model.go b/clients/netbox/model.go index be4767d..6ff0e6c 100644 --- a/clients/netbox/model.go +++ b/clients/netbox/model.go @@ -8,7 +8,12 @@ import ( type ApiError struct { Status int - Detail string `json:"detail"` + Detail string `json:"detail"` + Errors []ApiSubError `json:"errors"` +} + +type ApiSubError struct { + Message string `json:"message"` } func (e *ApiError) Error() string { -- cgit v1.2.3