From deacfad22dc6336ea1296e1c4a01965ab0ae99e1 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sun, 26 Mar 2023 15:05:58 +0200 Subject: [PATCH] * added GetIPAddresses function * pagination is handled in a more sane way * removed unnecessary lines in devcontainer.json * renamed main.go to ipam.go --- .devcontainer/devcontainer.json | 26 ----- client.go | 179 ++++++++++++++++++++++++++++++++ main.go => ipam.go | 75 ++++++++----- 3 files changed, 226 insertions(+), 54 deletions(-) create mode 100644 client.go rename main.go => ipam.go (53%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cd79083..31243fe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,6 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/go { "name": "Go", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/go:1.20", // Features to add to the dev container. More info: https://containers.dev/features. @@ -10,35 +9,10 @@ "ghcr.io/guiyomh/features/vim": {} }, - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - "settings": {}, - "extensions": [ - // "streetsidesoftware.code-spell-checker" - ] - } - }, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [9000], - - // Use 'portsAttributes' to set default properties for specific forwarded ports. - // More info: https://containers.dev/implementors/json_reference/#port-attributes - // "portsAttributes": { - // "9000": { - // "label": "Hello Remote World", - // "onAutoForward": "notify" - // } - // } - // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": { "download revive": "go install github.com/mgechev/revive@latest", "add githook": "echo revive -set_exit_status > .git/hooks/pre-commit", "chmod githook": "chmod +x .git/hooks/pre-commit" } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" } diff --git a/client.go b/client.go new file mode 100644 index 0000000..44973b1 --- /dev/null +++ b/client.go @@ -0,0 +1,179 @@ +// Package netboxapi provides custom functions for the default +// go-netbox package. It mostly handles pagination. +package netboxapi + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + e "git.stinnesbeck.com/nils/errorhandler" + transport "github.com/go-openapi/runtime/client" + "github.com/netbox-community/go-netbox/v3/netbox/client" +) + +// NetBoxAPI is the type we use to add custom functions +type NetBoxAPI struct { + api client.NetBoxAPI + Token string + URL string +} + +type apiResponse struct { + Count int64 `json:"count"` + Next string `json:"next"` + Previous string `json:"previous"` + Results []any `json:"results"` +} + +// NewNetBoxClient returns a client to NetBox at the given url, +// using the provided token +func NewNetBoxClient(token string, url string) (NetBoxAPI, error) { + // Construct the API basis and configure authentication + t := transport.New(url, client.DefaultBasePath, []string{"https"}) + t.DefaultAuthentication = transport.APIKeyAuth( + "Authorization", + "header", + fmt.Sprintf("Token %v", token), + ) + + // create NetBox api variable + n := client.New(t, nil) + + // create a new variable for our custom functions + nb := NetBoxAPI{ + api: *n, + Token: token, + URL: url, + } + return nb, nil +} + +func (nb NetBoxAPI) handleRawPagination(url string) (*apiResponse, error) { + var response apiResponse + b, err := nb.rawNetBoxAPICall(url, http.MethodGet) + if err != nil { + return nil, e.FormatError("can't perform raw NetBox API call", err) + } + + // Unmarshal json into response + if err := json.Unmarshal(b, &response); err != nil { + return nil, e.FormatError("can't unmarshal json payload in raw NetBox API call", err) + } + + // there are no more pages left, return the response we got + if response.Next == "" { + return &response, nil + } + + // there are more pages left, get more responses from there + + // recursive calling of handlePagination to get all following pages + nextResponse, err := nb.handleRawPagination(response.Next) + if err != nil { + return nil, e.FormatError("can't get next response from api", err) + } + + // append all next responses' results to this response's result + response.Results = append(response.Results, nextResponse.Results...) + + // go back up the chain through recursion + return &response, nil +} + +func (nb NetBoxAPI) handlePagination(url string, payload any) error { + response, err := nb.handleRawPagination(url) + if err != nil { + return e.FormatError("can't handle pagination for url "+url, err) + } + + fmt.Println(response.Count) + + // cast response into our payload + if err := resultToOtherStructure(*response, &payload); err != nil { + return e.FormatError("can't parse result to other Structure", err) + } + + // prettify.Print(payload) + return nil + +} + +func resultToOtherStructure(input apiResponse, output any) error { + // get JSON representation of results + c, err := json.Marshal(input.Results) + if err != nil { + return err + } + + // Unmarshal onto specific structure via output + return json.Unmarshal(c, &output) +} + +func (nb NetBoxAPI) rawNetBoxAPICall(url string, method string, data ...any) ([]byte, error) { + // construct complete URL (url already comes with a "/" after the "/api" part) + // url = fmt.Sprintf("https://%s/api%s", nb.URL, url) + + // log.Printf("calling API on url: %s with method %s and data %+v\n", url, method, data) + // create new transport client + + c := &http.Client{} + var req *http.Request + + switch method { + case http.MethodPatch, http.MethodPost, http.MethodPut: + // to patch something we need data, check it exists + if data == nil { + return nil, fmt.Errorf("no data was provided, please provide data to use PATCH") + } + + payload, err := json.Marshal(data) + if err != nil { + return nil, e.FormatError(fmt.Sprintf("can't marshal data %+v for apicall %s", data, url), err) + } + + // construct request + innerRequest, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + return nil, e.FormatError(fmt.Sprintf("can't construct new request (method: %s, URL: %s, payload %s)", method, url, payload), err) + } + + // hand over request to outer request + req = innerRequest + + case http.MethodGet: + innerRequest, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, e.FormatError(fmt.Sprintf("can't construct new request (method: %s, URL: %s)", method, url), err) + } + + // hand over request to outer request + req = innerRequest + } + + // set request headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Token %s", nb.Token)) + + // call API client with request + resp, err := c.Do(req) + if err != nil { + return nil, e.FormatError("can't get response from "+url, err) + } + defer resp.Body.Close() + + // read body of response + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, e.FormatError("can't read response body", err) + } + + switch resp.StatusCode { + case 200, 201: + return body, nil + default: + return body, fmt.Errorf("got bad StatusCode %d in response from API, data is:\n\t-> %+v", resp.StatusCode, data) + } +} diff --git a/main.go b/ipam.go similarity index 53% rename from main.go rename to ipam.go index e60ba5c..fa21ff8 100644 --- a/main.go +++ b/ipam.go @@ -1,40 +1,13 @@ -// Package netboxapi provides custom functions for the default -// go-netbox package. It mostly handles pagination. package netboxapi import ( "context" - "fmt" e "git.stinnesbeck.com/nils/errorhandler" - transport "github.com/go-openapi/runtime/client" - "github.com/netbox-community/go-netbox/v3/netbox/client" "github.com/netbox-community/go-netbox/v3/netbox/client/ipam" "github.com/netbox-community/go-netbox/v3/netbox/models" ) -// NetBoxAPI is the type we use to add custom functions -type NetBoxAPI client.NetBoxAPI - -// NewNetBoxClient returns a client to NetBox at the given url, -// using the provided token -func NewNetBoxClient(token string, url string) (NetBoxAPI, error) { - // Construct the API basis and configure authentication - t := transport.New(url, client.DefaultBasePath, []string{"https"}) - t.DefaultAuthentication = transport.APIKeyAuth( - "Authorization", - "header", - fmt.Sprintf("Token %v", token), - ) - - // create NetBox api variable - n := client.New(t, nil) - - // create a new variable for our custom functions - var nb NetBoxAPI = NetBoxAPI(*n) - return nb, nil -} - // GetPrefixes returns prefixes from the api filtered by the parameter list param func (nb NetBoxAPI) GetPrefixes(param ipam.IpamPrefixesListParams) ([]*models.Prefix, error) { // check if context was set, if not set background @@ -56,7 +29,7 @@ func (nb NetBoxAPI) GetPrefixes(param ipam.IpamPrefixesListParams) ([]*models.Pr } // get prefixes - prefixes, err := nb.Ipam.IpamPrefixesList(¶m, nil) + prefixes, err := nb.api.Ipam.IpamPrefixesList(¶m, nil) if err != nil { return nil, e.FormatError("can't get prefixes", err) } @@ -88,3 +61,49 @@ func (nb NetBoxAPI) GetPrefixes(param ipam.IpamPrefixesListParams) ([]*models.Pr return prefixesToReturn, nil } + +// GetIPAddresses returns IP addresses from the api filtered by the parameter list param +func (nb NetBoxAPI) GetIPAddresses(param ipam.IpamIPAddressesListParams) ([]*models.IPAddress, error) { + // check if context was set, if not set background + if param.Context == nil { + param.Context = context.Background() + } + + var defaultOffset int64 + var defaultLimit int64 = 50 + + // set Offset if not set before + if param.Limit == nil { + param.Limit = &defaultLimit + } + + // set Offset if not set before + if param.Offset == nil { + param.Offset = &defaultOffset + } + + // get addresses + addresses, err := nb.api.Ipam.IpamIPAddressesList(¶m, nil) + if err != nil { + return nil, e.FormatError("can't get prefixes", err) + } + + // check if there are no more prefixes on other pages + if addresses.Payload.Next == nil { + // return the results of the first batch, there are no others! + return addresses.Payload.Results, nil + } + + // variable to hold all prefixes to return + var addressesToReturn []*models.IPAddress + + if err := nb.handlePagination(addresses.Payload.Next.String(), &addressesToReturn); err != nil { + return nil, e.FormatError("can't handle Pagination", err) + } + + // append append first batch to the other batches + addressesToReturn = append(addressesToReturn, addresses.Payload.Results...) + + // return all addresses + return addressesToReturn, nil +}