Newer
Older
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package netbox
import (
)
// A Client is a NetBox client. It can be used to retrieve network and
// datacenter infrastructure information from a NetBox server.
type Client struct {
// DCIM provides access to methods in NetBox's DCIM API.
DCIM *DCIMService
// IPAM provides access to methods in NetBox's IPAM API.
IPAM *IPAMService
// Tenancy provides access to methods in NetBox's Tenancy API.
Tenancy *TenancyService
u *url.URL
client *http.Client
}
// NewClient returns a new instance of a NetBox client. addr specifies the address
// of the NetBox server, token specifies the api key to use,
// and client specifies an optional HTTP client to use for requests.
//
// If token is the empty string, no Authentication will be included in requests,
// providing anonymous read-only access depending on your NetBox config.
//
// If client is nil, a default HTTP client will be used.
func NewClient(addr string, token string, client *http.Client) (*Client, error) {
if client == nil {
client = &http.Client{}
}
// Append trailing slash there is none. This is necessary
// to be able to concat url parts in a correct manner.
// See NewRequest
if !strings.HasSuffix(addr, "/") {
addr = addr + "/"
}
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
c := &Client{
c.Tenancy = NewTenancyService(c)
return c, nil
}
// NewRequest creates a HTTP request using the input HTTP method, URL
// endpoint, and a Valuer which creates URL parameters for the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
return c.NewDataRequest(method, endpoint, options, nil)
}
// NewDataRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// a io.Reader as the body of the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
//
// If a nil io.Reader is specified, no body will be sent with the request.
func (c *Client) NewDataRequest(method string, endpoint string, options Valuer, body io.Reader) (*http.Request, error) {
// Allow specifying a base path for API requests, so if a NetBox server
// resides at a path like http://example.com/netbox/, API requests will
// be sent to http://example.com/netbox/api/...
//
// Enables support of: https://github.com/digitalocean/netbox/issues/212.
//
// Remove leading slash if there is one. This is necessary to be able to
// concat url parts in a correct manner. We can not use path.Join here,
// because this always trims the trailing slash, which causes the
// Do function to always run into 301 and then retry the correct
// Location. With GET, it does work with one useless request, but it breaks
// each other http method.
// Doing this, because out-of-tree extensions are more robust. If someone
// implements an own API-call, we do not override parts of c.u, even if
// the caller uses "/api/...".
rel, err := url.Parse(strings.TrimLeft(endpoint, "/"))
if err != nil {
return nil, err
}
u := c.u.ResolveReference(rel)
// If a valuer is specified, add the values as the query string
if options != nil {
values, err := options.Values()
if err != nil {
return nil, err
}
u.RawQuery = values.Encode()
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.token))
}
return req, nil
}
// NewJSONRequest creates a HTTP request using the input HTTP method, URL
// endpoint, a Valuer which creates URL parameters for the request, and
// an io.Reader as the body of the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
//
// The body parameter is marshaled to JSON and sent as a HTTP request body.
// Body must not be nil.
func (c *Client) NewJSONRequest(method string, endpoint string, options Valuer, body interface{}) (*http.Request, error) {
if body == nil {
return nil, errors.New("expected body to be not nil")
}
b := new(bytes.Buffer)
err := json.NewEncoder(b).Encode(body)
if err != nil {
return nil, err
}
req, err := c.NewDataRequest(
method,
endpoint,
options,
b,
)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
return req, nil
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
}
// Do executes an HTTP request and if v is not nil, Do unmarshals result
// JSON onto v.
func (c *Client) Do(req *http.Request, v interface{}) error {
res, err := c.client.Do(req)
if err != nil {
return err
}
defer func() {
_ = res.Body.Close()
}()
// Test if the request was successful.
// If not, do not try to decode the body. This would result in
// misleading error messages
err = httpStatusOK(res)
if err != nil {
return err
}
if v == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(v)
}
// httpStatusOK tests if the StatusCode of res is smaller than 300. Tries to
// Unmarshal the response into json, and returns only the detail
// if this exists. Otherwise returns the status code with the raw Body data.
func httpStatusOK(res *http.Response) error {
if res.StatusCode >= http.StatusMultipleChoices {
errDetail := struct {
Detail string `json:"detail"`
}{}
bodyData, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("%d - %v", res.StatusCode, err)
}
err = json.Unmarshal(bodyData, &errDetail)
if err == nil && errDetail.Detail != "" {
return fmt.Errorf("%d - %s", res.StatusCode, errDetail.Detail)
return fmt.Errorf("%d - %s", res.StatusCode, bodyData)
}
return nil
}