diff options
author | Mike Crute <mike@crute.us> | 2023-09-27 16:55:23 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2023-09-27 16:55:54 -0700 |
commit | 02f5fbcdd06c50d13c4f4df09fc29ca479dd492b (patch) | |
tree | 6bed705b6775f211273e595216c2f7aeca9d4f6d /clients | |
parent | f285d4b0d9018a5b256bcbb729c08bc87f09549e (diff) | |
download | golib-02f5fbcdd06c50d13c4f4df09fc29ca479dd492b.tar.bz2 golib-02f5fbcdd06c50d13c4f4df09fc29ca479dd492b.tar.xz golib-02f5fbcdd06c50d13c4f4df09fc29ca479dd492b.zip |
netbox: refactor out HTTP clientclients/netbox/v4.0.0
Diffstat (limited to 'clients')
-rw-r--r-- | clients/netbox/client.go | 70 | ||||
-rw-r--r-- | clients/netbox/go.mod | 4 | ||||
-rw-r--r-- | clients/netbox/gql_dns_servers.go | 47 | ||||
-rw-r--r-- | clients/netbox/http.go | 174 | ||||
-rw-r--r-- | clients/netbox/model.go | 7 |
5 files changed, 243 insertions, 59 deletions
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 | |||
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "context" | 4 | "context" |
5 | "encoding/json" | ||
6 | "fmt" | 5 | "fmt" |
7 | "io" | ||
8 | "net" | 6 | "net" |
9 | "net/http" | ||
10 | "net/url" | ||
11 | "strconv" | 7 | "strconv" |
12 | ) | 8 | ) |
13 | 9 | ||
@@ -18,49 +14,11 @@ type NetboxClient interface { | |||
18 | } | 14 | } |
19 | 15 | ||
20 | type BasicNetboxClient struct { | 16 | type BasicNetboxClient struct { |
21 | ApiKey string | 17 | *NetboxHttpClient |
22 | Endpoint string | ||
23 | } | 18 | } |
24 | 19 | ||
25 | var _ NetboxClient = (*BasicNetboxClient)(nil) | 20 | func MustNewBasicNetboxClient(apiKey, endpoint string) NetboxClient { |
26 | 21 | return &BasicNetboxClient{MustNewNetboxHttpClient(apiKey, endpoint)} | |
27 | func (c *BasicNetboxClient) makeRequestRaw(ctx context.Context, method, u string, ib io.Reader, o interface{}) error { | ||
28 | req, err := http.NewRequestWithContext(ctx, method, u, ib) | ||
29 | if err != nil { | ||
30 | return err | ||
31 | } | ||
32 | req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.ApiKey)) | ||
33 | |||
34 | res, err := http.DefaultClient.Do(req) | ||
35 | if err != nil { | ||
36 | return err | ||
37 | } | ||
38 | defer res.Body.Close() | ||
39 | |||
40 | if res.StatusCode != http.StatusOK { | ||
41 | apiError := &ApiError{Status: res.StatusCode} | ||
42 | if err = json.NewDecoder(res.Body).Decode(apiError); err != nil { | ||
43 | return fmt.Errorf("Netbox JSON decode error while parsing error with status %d: %w", res.StatusCode, err) | ||
44 | } | ||
45 | return apiError | ||
46 | } | ||
47 | |||
48 | if err = json.NewDecoder(res.Body).Decode(o); err != nil { | ||
49 | return err | ||
50 | } | ||
51 | |||
52 | return nil | ||
53 | } | ||
54 | |||
55 | func (c *BasicNetboxClient) makeRequest(ctx context.Context, method, path string, q url.Values, ib io.Reader, o interface{}) error { | ||
56 | u, err := url.Parse(c.Endpoint) | ||
57 | if err != nil { | ||
58 | return err | ||
59 | } | ||
60 | u.Path = path | ||
61 | u.RawQuery = q.Encode() | ||
62 | |||
63 | return c.makeRequestRaw(ctx, method, u.String(), ib, o) | ||
64 | } | 22 | } |
65 | 23 | ||
66 | func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site string, tag string) ([]*net.IPNet, error) { | 24 | 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 | |||
71 | 29 | ||
72 | out := []*net.IPNet{} | 30 | out := []*net.IPNet{} |
73 | 31 | ||
74 | q := url.Values{} | 32 | q := NewNetboxGetRequest("/api/ipam/prefixes/") |
75 | q.Add("site_id", strconv.Itoa(s)) | 33 | q.Add("site_id", strconv.Itoa(s)) |
76 | q.Add("tag", tag) | 34 | q.Add("tag", tag) |
77 | 35 | ||
78 | page := &PrefixList{} | 36 | page := &PrefixList{} |
79 | if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil { | 37 | if err := c.Do(ctx, q, page); err != nil { |
80 | return nil, err | 38 | return nil, err |
81 | } | 39 | } |
82 | 40 | ||
@@ -90,7 +48,7 @@ func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site str | |||
90 | 48 | ||
91 | for page.Next != "" { | 49 | for page.Next != "" { |
92 | page = &PrefixList{} | 50 | page = &PrefixList{} |
93 | if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { | 51 | if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { |
94 | return nil, err | 52 | return nil, err |
95 | } | 53 | } |
96 | 54 | ||
@@ -109,11 +67,11 @@ func (c *BasicNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site str | |||
109 | func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) ([]*net.IPNet, error) { | 67 | func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) ([]*net.IPNet, error) { |
110 | out := []*net.IPNet{} | 68 | out := []*net.IPNet{} |
111 | 69 | ||
112 | q := url.Values{} | 70 | q := NewNetboxGetRequest("/api/ipam/prefixes/") |
113 | q.Add("tag", tag) | 71 | q.Add("tag", tag) |
114 | 72 | ||
115 | page := &PrefixList{} | 73 | page := &PrefixList{} |
116 | if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil { | 74 | if err := c.Do(ctx, q, page); err != nil { |
117 | return nil, err | 75 | return nil, err |
118 | } | 76 | } |
119 | 77 | ||
@@ -127,7 +85,7 @@ func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) | |||
127 | 85 | ||
128 | for page.Next != "" { | 86 | for page.Next != "" { |
129 | page = &PrefixList{} | 87 | page = &PrefixList{} |
130 | if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { | 88 | if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { |
131 | return nil, err | 89 | return nil, err |
132 | } | 90 | } |
133 | 91 | ||
@@ -144,11 +102,11 @@ func (c *BasicNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) | |||
144 | } | 102 | } |
145 | 103 | ||
146 | func (c *BasicNetboxClient) resolveSiteNameToId(ctx context.Context, s string) (int, error) { | 104 | func (c *BasicNetboxClient) resolveSiteNameToId(ctx context.Context, s string) (int, error) { |
147 | q := url.Values{} | 105 | q := NewNetboxGetRequest("/api/dcim/sites/") |
148 | q.Add("name", s) | 106 | q.Add("name", s) |
149 | 107 | ||
150 | out := &SiteList{} | 108 | out := &SiteList{} |
151 | if err := c.makeRequest(ctx, http.MethodGet, "/api/dcim/sites/", q, nil, out); err != nil { | 109 | if err := c.Do(ctx, q, out); err != nil { |
152 | return 0, err | 110 | return 0, err |
153 | } | 111 | } |
154 | 112 | ||
@@ -162,11 +120,11 @@ func (c *BasicNetboxClient) resolveSiteNameToId(ctx context.Context, s string) ( | |||
162 | func (c *BasicNetboxClient) GetServicesForVm(ctx context.Context, vmName string) ([]*Service, error) { | 120 | func (c *BasicNetboxClient) GetServicesForVm(ctx context.Context, vmName string) ([]*Service, error) { |
163 | out := []*Service{} | 121 | out := []*Service{} |
164 | 122 | ||
165 | q := url.Values{} | 123 | q := NewNetboxGetRequest("/api/ipam/services/") |
166 | q.Add("virtual_machine", vmName) | 124 | q.Add("virtual_machine", vmName) |
167 | 125 | ||
168 | page := &ServiceList{} | 126 | page := &ServiceList{} |
169 | if err := c.makeRequest(ctx, http.MethodGet, "/api/ipam/services/", q, nil, page); err != nil { | 127 | if err := c.Do(ctx, q, page); err != nil { |
170 | return nil, err | 128 | return nil, err |
171 | } | 129 | } |
172 | 130 | ||
@@ -176,7 +134,7 @@ func (c *BasicNetboxClient) GetServicesForVm(ctx context.Context, vmName string) | |||
176 | 134 | ||
177 | for page.Next != "" { | 135 | for page.Next != "" { |
178 | page = &ServiceList{} | 136 | page = &ServiceList{} |
179 | if err := c.makeRequestRaw(ctx, http.MethodGet, page.Next, nil, page); err != nil { | 137 | if err := c.Do(ctx, &NetboxRawGet{page.Next}, page); err != nil { |
180 | return nil, err | 138 | return nil, err |
181 | } | 139 | } |
182 | 140 | ||
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 @@ | |||
1 | module code.crute.us/mcrute/golib/clients/netbox/v3 | 1 | module code.crute.us/mcrute/golib/clients/netbox/v4 |
2 | 2 | ||
3 | go 1.18 | 3 | go 1.20 |
4 | 4 | ||
5 | require ( | 5 | require ( |
6 | github.com/mitchellh/mapstructure v1.5.0 | 6 | 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 @@ | |||
1 | package netbox | ||
2 | |||
3 | const dnsServerGQLQuery = `fragment VMHostDetails on VMInterfaceType{ | ||
4 | virtual_machine { | ||
5 | site { | ||
6 | name | ||
7 | } | ||
8 | } | ||
9 | } | ||
10 | |||
11 | fragment HostDetails on InterfaceType { | ||
12 | device { | ||
13 | site { | ||
14 | name | ||
15 | } | ||
16 | } | ||
17 | } | ||
18 | |||
19 | query { | ||
20 | ip_address_list(tag:"dns-server") { | ||
21 | address | ||
22 | assigned_object{ | ||
23 | ...VMHostDetails | ||
24 | ...HostDetails | ||
25 | } | ||
26 | } | ||
27 | }` | ||
28 | |||
29 | type dnsServerGQLResponse struct { | ||
30 | Data struct { | ||
31 | AddressList []struct { | ||
32 | Address string `json:"address"` | ||
33 | AssignedObject struct { | ||
34 | VirtualMachine struct { | ||
35 | Site struct { | ||
36 | Name string `json:"name"` | ||
37 | } `json:"site"` | ||
38 | } `json:"virtual_machine"` | ||
39 | Device struct { | ||
40 | Site struct { | ||
41 | Name string `json:"name"` | ||
42 | } `json:"site"` | ||
43 | } `json:"device"` | ||
44 | } `json:"assigned_object"` | ||
45 | } `json:"ip_address_list"` | ||
46 | } `json:"data"` | ||
47 | } | ||
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 @@ | |||
1 | package netbox | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "context" | ||
6 | "encoding/json" | ||
7 | "fmt" | ||
8 | "net/http" | ||
9 | "net/url" | ||
10 | ) | ||
11 | |||
12 | // NetboxHttpClient is an HTTP client for the Netbox API. It is very | ||
13 | // low-level and should not be consumed by most clients. Instead use a | ||
14 | // client implementing NetboxClient. | ||
15 | type NetboxHttpClient struct { | ||
16 | ApiKey string | ||
17 | Endpoint url.URL | ||
18 | } | ||
19 | |||
20 | func MustNewNetboxHttpClient(apiKey, endpoint string) *NetboxHttpClient { | ||
21 | u, err := url.Parse(endpoint) | ||
22 | if err != nil { | ||
23 | panic(err) | ||
24 | } | ||
25 | return &NetboxHttpClient{apiKey, *u} | ||
26 | } | ||
27 | |||
28 | func (c *NetboxHttpClient) Do(ctx context.Context, r NetboxRequest, out any) error { | ||
29 | u := c.Endpoint // Store a local copy, requests may mutate it | ||
30 | req, err := r.BuildRequest(ctx, &u) | ||
31 | if err != nil { | ||
32 | return err | ||
33 | } | ||
34 | |||
35 | req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.ApiKey)) | ||
36 | |||
37 | res, err := http.DefaultClient.Do(req) | ||
38 | if err != nil { | ||
39 | return err | ||
40 | } | ||
41 | defer res.Body.Close() | ||
42 | |||
43 | if res.StatusCode != http.StatusOK { | ||
44 | apiError := &ApiError{Status: res.StatusCode} | ||
45 | if err = json.NewDecoder(res.Body).Decode(apiError); err != nil { | ||
46 | return fmt.Errorf("Netbox JSON decode error while parsing error with status %d: %w", res.StatusCode, err) | ||
47 | } | ||
48 | return apiError | ||
49 | } | ||
50 | |||
51 | if out != nil { | ||
52 | return json.NewDecoder(res.Body).Decode(out) | ||
53 | } | ||
54 | |||
55 | return nil | ||
56 | } | ||
57 | |||
58 | type NetboxRequest interface { | ||
59 | BuildRequest(context.Context, *url.URL) (*http.Request, error) | ||
60 | } | ||
61 | |||
62 | type NetboxGraphQLRequest struct { | ||
63 | Query string | ||
64 | } | ||
65 | |||
66 | var _ NetboxRequest = (*NetboxGraphQLRequest)(nil) | ||
67 | |||
68 | func (r *NetboxGraphQLRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { | ||
69 | q, err := json.Marshal(map[string]string{"query": r.Query}) | ||
70 | if err != nil { | ||
71 | return nil, err | ||
72 | } | ||
73 | |||
74 | host.Path = "/graphql/" | ||
75 | |||
76 | req, err := http.NewRequestWithContext( | ||
77 | ctx, | ||
78 | http.MethodPost, | ||
79 | host.String(), | ||
80 | bytes.NewBuffer(q), | ||
81 | ) | ||
82 | if err != nil { | ||
83 | return nil, err | ||
84 | } | ||
85 | |||
86 | req.Header.Add("Content-Type", "application/json") | ||
87 | req.Header.Add("Accept", "application/json") | ||
88 | |||
89 | return req, nil | ||
90 | } | ||
91 | |||
92 | type NetboxGetRequest struct { | ||
93 | url.Values | ||
94 | Path string | ||
95 | } | ||
96 | |||
97 | var _ NetboxRequest = (*NetboxGetRequest)(nil) | ||
98 | |||
99 | func NewNetboxGetRequest(path string) *NetboxGetRequest { | ||
100 | return &NetboxGetRequest{url.Values{}, path} | ||
101 | } | ||
102 | |||
103 | func (r *NetboxGetRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { | ||
104 | host.Path = r.Path | ||
105 | host.RawQuery = r.Encode() | ||
106 | |||
107 | req, err := http.NewRequestWithContext( | ||
108 | ctx, | ||
109 | http.MethodGet, | ||
110 | host.String(), | ||
111 | nil, | ||
112 | ) | ||
113 | if err != nil { | ||
114 | return nil, err | ||
115 | } | ||
116 | |||
117 | req.Header.Add("Accept", "application/json") | ||
118 | |||
119 | return req, nil | ||
120 | } | ||
121 | |||
122 | type NetboxRawGet struct { | ||
123 | Url string | ||
124 | } | ||
125 | |||
126 | var _ NetboxRequest = (*NetboxRawGet)(nil) | ||
127 | |||
128 | func (r *NetboxRawGet) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { | ||
129 | req, err := http.NewRequestWithContext( | ||
130 | ctx, | ||
131 | http.MethodGet, | ||
132 | r.Url, | ||
133 | nil, | ||
134 | ) | ||
135 | if err != nil { | ||
136 | return nil, err | ||
137 | } | ||
138 | |||
139 | req.Header.Add("Accept", "application/json") | ||
140 | |||
141 | return req, nil | ||
142 | } | ||
143 | |||
144 | type NetboxJsonRequest struct { | ||
145 | Path string | ||
146 | Method string | ||
147 | Body any | ||
148 | } | ||
149 | |||
150 | var _ NetboxRequest = (*NetboxJsonRequest)(nil) | ||
151 | |||
152 | func (r *NetboxJsonRequest) BuildRequest(ctx context.Context, host *url.URL) (*http.Request, error) { | ||
153 | host.Path = r.Path | ||
154 | |||
155 | body, err := json.Marshal(r.Body) | ||
156 | if err != nil { | ||
157 | return nil, err | ||
158 | } | ||
159 | |||
160 | req, err := http.NewRequestWithContext( | ||
161 | ctx, | ||
162 | r.Method, | ||
163 | host.String(), | ||
164 | bytes.NewBuffer(body), | ||
165 | ) | ||
166 | if err != nil { | ||
167 | return nil, err | ||
168 | } | ||
169 | |||
170 | req.Header.Add("Content-Type", "application/json") | ||
171 | req.Header.Add("Accept", "application/json") | ||
172 | |||
173 | return req, nil | ||
174 | } | ||
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 ( | |||
8 | 8 | ||
9 | type ApiError struct { | 9 | type ApiError struct { |
10 | Status int | 10 | Status int |
11 | Detail string `json:"detail"` | 11 | Detail string `json:"detail"` |
12 | Errors []ApiSubError `json:"errors"` | ||
13 | } | ||
14 | |||
15 | type ApiSubError struct { | ||
16 | Message string `json:"message"` | ||
12 | } | 17 | } |
13 | 18 | ||
14 | func (e *ApiError) Error() string { | 19 | func (e *ApiError) Error() string { |