package netboxgo import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strings" "time" "github.com/pkg/errors" ) // Client struct is used to create a new NetBox endpoint type Client struct { // DCIM *DCIM // Tenancy *Tenancy // IPAM *IPAM // Virtualization *Virtualization // Secret *Secret Tenants *TenantsService Sites *SitesService SiteGroups *SiteGroupsService VirtualMachines *VirtualMachinesService Clusters *ClustersService VLANs *VLANsService Secrets *SecretsService InventoryItems *InventoryItemsService Devices *DevicesService DeviceRoles *DeviceRolesService DeviceTypes *DeviceTypesService Interfaces *InterfacesService Prefixes *PrefixesService VRFs *VRFsService RearPorts *RearPortsService // baseURL is the URL used for the base URL of the API baseURL *url.URL // httpClient is used for HTTP requests httpClient *http.Client // Reuse a single struct instead of allocating one for each service on the heap. common service // UserAgent is used to set the UserAgent on the HTTP requests UserAgent string // Token is set for authentication of the API Token string // TokenPrefix is optional to set token prefix other than "Token" TokenPrefix string // SessionKey is used to read authentication data SessionKey string // Used by golang wasm FetchMode string // Debug enables verbose debugging messages to console. Debug bool // InsecureSkipVerify is used to selectively skip InsecureVerifications InsecureSkipVerify bool } type service struct { client *Client } const ( circuitsPath = "/circuits" dcimPath = "/dcim" extrasPath = "/extras" ipamPath = "/ipam" tenancyPath = "/tenancy" usersPath = "/users" virtualizationPath = "/virtualization" ) // NetBoxSessionKey sets the session key for secrets retrieval type NetBoxSessionKey struct { XSessionKey string `json:"session_key"` } // NewClient returns a new NetBox API client func NewClient(apiurl string, httpClient *http.Client) (*Client, error) { c := &Client{} api, err := url.Parse(apiurl) if err != nil { return c, fmt.Errorf("unable to parse url %s: %w", apiurl, err) } c.baseURL = api c.SetDebug(false) c.SetUserAgent("netboxgo/0.0.6") c.httpClient = httpClient if httpClient == nil { transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: false, CipherSuites: []uint16{ tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, }, MinVersion: tls.VersionTLS12, }, Proxy: http.ProxyFromEnvironment, } timeout := time.Duration(60 * time.Second) c.httpClient = &http.Client{ Transport: transport, Jar: nil, Timeout: timeout, } } c.common.client = c c.Clusters = (*ClustersService)(&c.common) c.Devices = (*DevicesService)(&c.common) c.DeviceRoles = (*DeviceRolesService)(&c.common) c.DeviceTypes = (*DeviceTypesService)(&c.common) c.Interfaces = (*InterfacesService)(&c.common) c.InventoryItems = (*InventoryItemsService)(&c.common) c.Prefixes = (*PrefixesService)(&c.common) c.RearPorts = (*RearPortsService)(&c.common) c.Tenants = (*TenantsService)(&c.common) c.Secrets = (*SecretsService)(&c.common) c.Sites = (*SitesService)(&c.common) c.SiteGroups = (*SiteGroupsService)(&c.common) c.VirtualMachines = (*VirtualMachinesService)(&c.common) c.VLANs = (*VLANsService)(&c.common) c.VRFs = (*VRFsService)(&c.common) return c, nil } // SetUserAgent is used to set the user agent for interaction with the API func (c *Client) SetUserAgent(u string) { c.UserAgent = u } // SetDebug is used to toggle debugging, set to true for debug func (c *Client) SetDebug(b bool) { c.Debug = b } // SetToken is used to set the http token bearer for interaction with the API func (c *Client) SetToken(token string) { c.Token = token } // FetchSessionKey fetches sessionkey func (c *Client) FetchSessionKey(privatekey string) error { form := url.Values{} form.Add("private_key", privatekey) query := form.Encode() ctx := context.Background() req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL.String()+secretsPath+"/get-session-key/", strings.NewReader(query)) if err != nil { return err } req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/x-www-form-urlencoded") if c.FetchMode != "" { req.Header.Add("js.fetch:mode", "no-cors") } req.Header.Add("Authorization", " Token "+c.Token) var sessionkey NetBoxSessionKey res, err := c.do(req, &sessionkey) if err != nil { return err } if res.StatusCode != http.StatusOK { return errors.Errorf("response was: %d should be %d\n%s\n", res.StatusCode, http.StatusOK, res.Header) } c.SessionKey = sessionkey.XSessionKey return nil } // newRequest is used to make new request to endpoints func (c *Client) newRequest(ctx context.Context, method, path string, query string, body interface{}) (*http.Request, error) { var err error var buf io.ReadWriter u := &url.URL{ Scheme: c.baseURL.Scheme, Host: c.baseURL.Host, Path: c.baseURL.Path + path, RawQuery: query, } if body != nil { buf = new(bytes.Buffer) err = json.NewEncoder(buf).Encode(body) if err != nil { return nil, err } } readBuf, _ := json.Marshal(body) c.debugf("debug: method:%s url:%s body:%s", method, u.String(), string(readBuf)) req, err := http.NewRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } if c.SessionKey != "" { req.Header.Add("X-Session-Key", c.SessionKey) } req.Header.Set("Accept", "application/json") if c.UserAgent != "" { req.Header.Set("User-Agent", c.UserAgent) } if c.Token != "" { req.Header.Add("Authorization", "Token "+c.Token) } if c.Token != "" && c.TokenPrefix != "" { req.Header.Add("Authorization", c.TokenPrefix+" "+c.Token) } if c.SessionKey != "" { req.Header.Add("X-Session-Key", c.SessionKey) } return req, nil } // do method is used to decode request response to proper struct func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) { var err error var resp *http.Response resp, err = c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("client unable to do request: %w", err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { var data []byte data, err = io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read body response: %w", err) } return nil, fmt.Errorf("request failed: body=%s", string(data)) } if v != nil { err = json.NewDecoder(resp.Body).Decode(v) if err != nil { return resp, fmt.Errorf("unable to decode response body: %w", err) } } return resp, nil } // debugf is used as a formtter for debugging information func (c *Client) debugf(fmt string, v ...interface{}) { if c.Debug { log.Printf("cendot client: "+fmt, v...) } }