diff --git a/internal/source/fmc/client/api.go b/internal/source/fmc/client/api.go new file mode 100644 index 0000000..77f11d6 --- /dev/null +++ b/internal/source/fmc/client/api.go @@ -0,0 +1,308 @@ +package client + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "time" +) + +const ( + maxRetries = 5 + initialBackoff = 500 * time.Millisecond + backoffFactor = 2.0 + maxBackoff = 16 * time.Second +) + +// exponentialBackoff calculates the backoff duration based on the number of attempts. +func exponentialBackoff(attempt int) time.Duration { + backoff := time.Duration(float64(initialBackoff) * math.Pow(backoffFactor, float64(attempt))) + if backoff > maxBackoff { + backoff = maxBackoff + } + return backoff +} + +// Authenticate performs authentication on FMC API. If successful it returns access and refresh tokens. +func (fmcc FMCClient) Authenticate() (string, string, error) { + var ( + accessToken string + refreshToken string + err error + ) + + for attempt := 0; attempt < maxRetries; attempt++ { + accessToken, refreshToken, err = fmcc.authenticateOnce() + if err == nil { + return accessToken, refreshToken, nil + } + + fmcc.Logger.Debugf(fmcc.Ctx, "authentication attempt %d failed: %s", attempt, err) + time.Sleep(exponentialBackoff(attempt)) + } + + return "", "", fmt.Errorf("authentication failed after %d attempts: %w", maxRetries, err) +} + +// Helper function to Authenticate. Performs single attempt to authenticate to fmc api. +func (fmcc FMCClient) authenticateOnce() (string, string, error) { + ctx, cancel := context.WithTimeout(context.Background(), fmcc.DefaultTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/fmc_platform/v1/auth/generatetoken", fmcc.BaseURL), nil) + if err != nil { + return "", "", fmt.Errorf("new request with context: %w", err) + } + + // Add Basic authentication header + auth := fmt.Sprintf("%s:%s", fmcc.Username, fmcc.Password) + auth = base64.StdEncoding.EncodeToString([]byte(auth)) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth)) + + res, err := fmcc.HTTPClient.Do(req) + if err != nil { + return "", "", fmt.Errorf("req err: %w", err) + } + defer res.Body.Close() // Close the response body + + // Extract access and refresh tokens from response + accessToken := res.Header.Get("X-auth-access-token") + refreshToken := res.Header.Get("X-auth-refresh-token") + if accessToken == "" || refreshToken == "" { + return "", "", fmt.Errorf("failed extracting access and refresh tokens from response") //nolint:goerr113 + } + return accessToken, refreshToken, nil +} + +// MakeRequest sends an HTTP request to the specified path using the given method and body. +// It retries the request with exponential backoff up to a maximum number of attempts. +// If the request fails after the maximum number of attempts, it returns an error. +func (fmcc *FMCClient) MakeRequest(ctx context.Context, method, path string, body io.Reader, result interface{}) error { + var ( + resp *http.Response + err error + tokenRefreshed bool + ) + + for attempt := 0; attempt < maxRetries; attempt++ { + if ctx.Err() != nil { + fmcc.Logger.Debugf(ctx, "context canceled or expired: %s", ctx.Err()) + return ctx.Err() + } + + resp, err = fmcc.makeRequestOnce(ctx, method, path, body) + if err != nil { + fmcc.Logger.Debugf(ctx, "request attempt %d failed: %s", attempt, err) + time.Sleep(exponentialBackoff(attempt)) + continue + } + + // Check if the status code is 401 Unauthorized + if resp.StatusCode == http.StatusUnauthorized { + if !tokenRefreshed { + fmcc.Logger.Debugf(ctx, "received 401 Unauthorized, attempting to refresh token") + + accessToken, refreshToken, authErr := fmcc.Authenticate() + if authErr != nil { + return fmt.Errorf("failed to refresh token: %w", authErr) + } + + // Update the FMCClient with the new tokens. + fmcc.AccessToken = accessToken + fmcc.RefreshToken = refreshToken + + tokenRefreshed = true // Mark that the token has been refreshed. + continue // Retry the request immediately after refreshing the token. + } + // If the token has already been refreshed, return the 401 error. + return fmt.Errorf("request failed with 401 Unauthorized after token refresh") + } + + // Process the response if it's not a 401 + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + err = json.Unmarshal(bodyBytes, result) + if err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return nil + } + + return fmt.Errorf("request failed after %d attempts: %w", maxRetries, err) +} + +// makeRequestOnce sends an HTTP request to the specified path using the given method and body. +// It is a helper function for MakeRequest that sends the request only once. +func (fmcc *FMCClient) makeRequestOnce(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, fmcc.DefaultTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctxWithTimeout, method, fmt.Sprintf("%s/%s", fmcc.BaseURL, path), body) + if err != nil { + return nil, err + } + // Set the Authorization header. + req.Header.Set("X-auth-access-token", fmcc.AccessToken) + return fmcc.HTTPClient.Do(req) +} + +// GetDomains returns a list of domains from the FMC API. +// It sends a GET request to the /fmc_platform/v1/info/domain endpoint. +func (fmcc *FMCClient) GetDomains() ([]Domain, error) { + offset := 0 + limit := 25 + domains := []Domain{} + ctx := context.Background() + + for { + var marshaledResponse APIResponse[Domain] + err := fmcc.MakeRequest(ctx, http.MethodGet, fmt.Sprintf("fmc_platform/v1/info/domain?offset=%d&limit=%d", offset, limit), nil, &marshaledResponse) + if err != nil { + return nil, fmt.Errorf("make request for domains: %w", err) + } + + if len(marshaledResponse.Items) > 0 { + domains = append(domains, marshaledResponse.Items...) + } + + if len(marshaledResponse.Items) < limit { + break + } + offset += limit + } + + return domains, nil +} + +// GetDevices returns a list of devices from the FMC API for the specified domain. +func (fmcc *FMCClient) GetDevices(domainUUID string) ([]Device, error) { + offset := 0 + limit := 25 + devices := []Device{} + ctx := context.Background() + + for { + devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords?offset=%d&limit=%d", domainUUID, offset, limit) + var marshaledResponse APIResponse[Device] + err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil, &marshaledResponse) + if err != nil { + return nil, fmt.Errorf("make request for devices: %w", err) + } + + if len(marshaledResponse.Items) > 0 { + devices = append(devices, marshaledResponse.Items...) + } + + if len(marshaledResponse.Items) < limit { + break + } + offset += limit + } + + return devices, nil +} + +// GetDevicePhysicalInterfaces returns a list of physical interfaces for the specified device in the specified domain. +func (fmcc *FMCClient) GetDevicePhysicalInterfaces(domainUUID string, deviceID string) ([]PhysicalInterface, error) { + offset := 0 + limit := 25 + pIfaces := []PhysicalInterface{} + ctx := context.Background() + + for { + pInterfacesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/physicalinterfaces?offset=%d&limit=%d", domainUUID, deviceID, offset, limit) + var marshaledResponse APIResponse[PhysicalInterface] + err := fmcc.MakeRequest(ctx, http.MethodGet, pInterfacesURL, nil, &marshaledResponse) + if err != nil { + return nil, fmt.Errorf("make request for physical interfaces: %w", err) + } + + if len(marshaledResponse.Items) > 0 { + pIfaces = append(pIfaces, marshaledResponse.Items...) + } + + if len(marshaledResponse.Items) < limit { + break + } + offset += limit + } + + return pIfaces, nil +} + +func (fmcc *FMCClient) GetDeviceVLANInterfaces(domainUUID string, deviceID string) ([]VlanInterface, error) { + offset := 0 + limit := 25 + vlanIfaces := []VlanInterface{} + ctx := context.Background() + + for { + pInterfacesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/vlaninterfaces?offset=%d&limit=%d", domainUUID, deviceID, offset, limit) + var marshaledResponse APIResponse[VlanInterface] + err := fmcc.MakeRequest(ctx, http.MethodGet, pInterfacesURL, nil, &marshaledResponse) + if err != nil { + return nil, fmt.Errorf("make request for VLAN interfaces: %w", err) + } + + if len(marshaledResponse.Items) > 0 { + vlanIfaces = append(vlanIfaces, marshaledResponse.Items...) + } + + if len(marshaledResponse.Items) < limit { + break + } + offset += limit + } + + return vlanIfaces, nil +} + +func (fmcc *FMCClient) GetPhysicalInterfaceInfo(domainUUID string, deviceID string, interfaceID string) (*PhysicalInterfaceInfo, error) { + var pInterfaceInfo PhysicalInterfaceInfo + ctx := context.Background() + + devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/physicalinterfaces/%s", domainUUID, deviceID, interfaceID) + err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil, &pInterfaceInfo) + if err != nil { + return nil, fmt.Errorf("make request for physical interface info: %w", err) + } + + return &pInterfaceInfo, nil +} + +func (fmcc *FMCClient) GetVLANInterfaceInfo(domainUUID string, deviceID string, interfaceID string) (*VLANInterfaceInfo, error) { + var vlanInterfaceInfo VLANInterfaceInfo + ctx := context.Background() + + devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/vlaninterfaces/%s", domainUUID, deviceID, interfaceID) + err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil, &vlanInterfaceInfo) + if err != nil { + return nil, fmt.Errorf("make request for VLAN interface info: %w", err) + } + + return &vlanInterfaceInfo, nil +} + +func (fmcc *FMCClient) GetDeviceInfo(domainUUID string, deviceID string) (*DeviceInfo, error) { + var deviceInfo DeviceInfo + ctx := context.Background() + + devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s", domainUUID, deviceID) + err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil, &deviceInfo) + if err != nil { + return nil, fmt.Errorf("make request for device info: %w", err) + } + + return &deviceInfo, nil +} diff --git a/internal/source/fmc/client/client.go b/internal/source/fmc/client/client.go new file mode 100644 index 0000000..3c54934 --- /dev/null +++ b/internal/source/fmc/client/client.go @@ -0,0 +1,47 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/bl4ko/netbox-ssot/internal/constants" + "github.com/bl4ko/netbox-ssot/internal/logger" +) + +type FMCClient struct { + HTTPClient *http.Client + BaseURL string + Username string + Password string + AccessToken string + RefreshToken string + DefaultTimeout time.Duration + Logger *logger.Logger + Ctx context.Context +} + +// NewFMCClient creates a new FMC client with the given parameters. +// It authenticates to the FMC API and stores the access and refresh tokens. +func NewFMCClient(context context.Context, username string, password string, httpScheme string, hostname string, port int, httpClient *http.Client, logger *logger.Logger) (*FMCClient, error) { + c := &FMCClient{ + HTTPClient: httpClient, + BaseURL: fmt.Sprintf("%s://%s:%d/api", httpScheme, hostname, port), + Username: username, + Password: password, + DefaultTimeout: time.Second * constants.DefaultAPITimeout, + Logger: logger, + Ctx: context, + } + + aToken, rToken, err := c.Authenticate() + if err != nil { + return nil, fmt.Errorf("authentication: %w", err) + } + + c.AccessToken = aToken + c.RefreshToken = rToken + + return c, nil +} diff --git a/internal/source/fmc/client/fmc_structs.go b/internal/source/fmc/client/fmc_structs.go new file mode 100644 index 0000000..aa21676 --- /dev/null +++ b/internal/source/fmc/client/fmc_structs.go @@ -0,0 +1,125 @@ +package client + +// VLANInterfaceInfo represents information about a VLAN interface. +type VLANInterfaceInfo struct { + Type string `json:"type"` + Mode string `json:"mode"` + VID int `json:"vlanId"` + MTU int `json:"MTU"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + ID string `json:"id"` + Description string `json:"description"` + Hardware *struct { + Speed string `json:"speed"` + Duplex string `json:"duplex"` + } `json:"hardware"` + SecurityZone *struct { + ID string `json:"id"` + Type string `json:"type"` + } `json:"securityZone"` + IPv4 *struct { + Static *struct { + Address string `json:"address"` + Netmask string `json:"netmask"` + } `json:"static"` + } `json:"ipv4"` + IPv6 *struct { + EnableIPv6 bool `json:"enableIPV6"` + } `json:"ipv6"` +} + +// DeviceInfo represents information about a FMC device. +type DeviceInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Model string `json:"model"` + ModelID string `json:"modelId"` + ModelNumber string `json:"modelNumber"` + SWVersion string `json:"sw_version"` + Hostname string `json:"hostName"` + Metadata struct { + SerialNumber string `json:"deviceSerialNumber"` + InventoryData struct { + CPUCores string `json:"cpuCores"` + CPUType string `json:"cpuType"` + MemoryInMB string `json:"memoryInMB"` + } `json:"inventoryData"` + } `json:"metadata"` +} + +// VlanInterface represents a VLAN interface. +type VlanInterface struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` +} + +// PhysicalInterface represents a physical interface. +type PhysicalInterface struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` +} + +// PaginationResponse represents the paging information in the API response. +type PaginationResponse struct { + Offset int `json:"offset"` + Limit int `json:"limit"` + Count int `json:"count"` + Pages int `json:"pages"` +} + +// LinksResponse represents the links in the API response. +type LinksResponse struct { + Self string `json:"self"` +} + +// APIResponse represents the API response. +type APIResponse[T any] struct { + Links LinksResponse `json:"links"` + Paging PaginationResponse `json:"paging"` + Items []T `json:"items"` +} + +// Domain represents a domain in FMC. +type Domain struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Type string `json:"type"` +} + +// Device represents a device in FMC. +type Device struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` +} + +// PhysicalInterfaceInfo represents information about a physical interface. +type PhysicalInterfaceInfo struct { + Type string `json:"type"` + MTU int `json:"MTU"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + ID string `json:"id"` + Mode string `json:"mode"` + Description string `json:"description"` + Hardware *struct { + Speed string `json:"speed"` + Duplex string `json:"duplex"` + } `json:"hardware"` + SecurityZone *struct { + ID string `json:"id"` + Type string `json:"type"` + } `json:"securityZone"` + IPv4 *struct { + Static *struct { + Address string `json:"address"` + Netmask string `json:"netmask"` + } `json:"static"` + } `json:"ipv4"` + IPv6 *struct { + EnableIPv6 bool `json:"enableIPV6"` + } `json:"ipv6"` +} diff --git a/internal/source/fmc/fmc.go b/internal/source/fmc/fmc.go index c2999a3..d7534a7 100644 --- a/internal/source/fmc/fmc.go +++ b/internal/source/fmc/fmc.go @@ -7,6 +7,7 @@ import ( "github.com/bl4ko/netbox-ssot/internal/netbox/inventory" "github.com/bl4ko/netbox-ssot/internal/netbox/objects" "github.com/bl4ko/netbox-ssot/internal/source/common" + "github.com/bl4ko/netbox-ssot/internal/source/fmc/client" "github.com/bl4ko/netbox-ssot/internal/utils" ) @@ -17,10 +18,10 @@ type FMCSource struct { common.Config // FMC data. Initialized in init functions. - Domains map[string]Domain - Devices map[string]*DeviceInfo - DevicePhysicalIfaces map[string][]*PhysicalInterfaceInfo - DeviceVlanIfaces map[string][]*VLANInterfaceInfo + Domains map[string]client.Domain + Devices map[string]*client.DeviceInfo + DevicePhysicalIfaces map[string][]*client.PhysicalInterfaceInfo + DeviceVlanIfaces map[string][]*client.VLANInterfaceInfo // Netbox devices representing firewalls. NBDevices map[string]*objects.Device @@ -38,7 +39,7 @@ func (fmcs *FMCSource) Init() error { return fmt.Errorf("create new http client: %s", err) } - c, err := newFMCClient(fmcs.SourceConfig.Username, fmcs.SourceConfig.Password, string(fmcs.SourceConfig.HTTPScheme), fmcs.SourceConfig.Hostname, fmcs.SourceConfig.Port, httpClient) + c, err := client.NewFMCClient(fmcs.Ctx, fmcs.SourceConfig.Username, fmcs.SourceConfig.Password, string(fmcs.SourceConfig.HTTPScheme), fmcs.SourceConfig.Hostname, fmcs.SourceConfig.Port, httpClient, fmcs.Logger) if err != nil { return fmt.Errorf("create FMC client: %s", err) } @@ -53,7 +54,13 @@ func (fmcs *FMCSource) Init() error { fmcs.HostSiteRelations = utils.ConvertStringsToRegexPairs(fmcs.SourceConfig.HostSiteRelations) fmcs.Logger.Debugf(fmcs.Ctx, "HostSiteRelations: %s", fmcs.HostSiteRelations) - initFunctions := []func(*fmcClient) error{ + // Init FMC objects + fmcs.Domains = make(map[string]client.Domain) + fmcs.Devices = make(map[string]*client.DeviceInfo) + fmcs.DevicePhysicalIfaces = make(map[string][]*client.PhysicalInterfaceInfo) + fmcs.DeviceVlanIfaces = make(map[string][]*client.VLANInterfaceInfo) + + initFunctions := []func(*client.FMCClient) error{ fmcs.initObjects, } for _, initFunc := range initFunctions { diff --git a/internal/source/fmc/fmc_client.go b/internal/source/fmc/fmc_client.go deleted file mode 100644 index 8e430b7..0000000 --- a/internal/source/fmc/fmc_client.go +++ /dev/null @@ -1,503 +0,0 @@ -package fmc - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "math" - "net/http" - "time" - - "github.com/bl4ko/netbox-ssot/internal/constants" -) - -type fmcClient struct { - HTTPClient *http.Client - BaseURL string - Username string - Password string - AccessToken string - RefreshToken string - DefaultTimeout time.Duration -} - -const ( - maxRetries = 5 - initialBackoff = 500 * time.Millisecond - backoffFactor = 2.0 - maxBackoff = 16 * time.Second -) - -func newFMCClient(username string, password string, httpScheme string, hostname string, port int, httpClient *http.Client) (*fmcClient, error) { - // First we obtain access and refresh token - c := &fmcClient{ - HTTPClient: httpClient, - BaseURL: fmt.Sprintf("%s://%s:%d/api", httpScheme, hostname, port), - Username: username, - Password: password, - DefaultTimeout: time.Second * constants.DefaultAPITimeout, - } - - aToken, rToken, err := c.Authenticate() - if err != nil { - return nil, fmt.Errorf("authentication: %w", err) - } - - c.AccessToken = aToken - c.RefreshToken = rToken - - return c, nil -} - -// exponentialBackoff calculates the backoff duration based on the number of attempts. -func exponentialBackoff(attempt int) time.Duration { - backoff := time.Duration(float64(initialBackoff) * math.Pow(backoffFactor, float64(attempt))) - if backoff > maxBackoff { - backoff = maxBackoff - } - return backoff -} - -// Authenticate performs authentication on FMC API. If successful it returns access and refresh tokens. -func (fmcc fmcClient) Authenticate() (string, string, error) { - var ( - accessToken string - refreshToken string - err error - ) - - for attempt := 0; attempt < maxRetries; attempt++ { - accessToken, refreshToken, err = fmcc.authenticateOnce() - if err == nil { - return accessToken, refreshToken, nil - } - - time.Sleep(exponentialBackoff(attempt)) - } - - return "", "", fmt.Errorf("authentication failed after %d attempts: %w", maxRetries, err) -} - -func (fmcc fmcClient) authenticateOnce() (string, string, error) { - ctx, cancel := context.WithTimeout(context.Background(), fmcc.DefaultTimeout) - defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/fmc_platform/v1/auth/generatetoken", fmcc.BaseURL), nil) - if err != nil { - return "", "", fmt.Errorf("new request with context: %w", err) - } - - // Add Basic authentication header - auth := fmt.Sprintf("%s:%s", fmcc.Username, fmcc.Password) - auth = base64.StdEncoding.EncodeToString([]byte(auth)) - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth)) - - res, err := fmcc.HTTPClient.Do(req) - if err != nil { - return "", "", fmt.Errorf("req err: %w", err) - } - defer res.Body.Close() // Close the response body - - // Extract access and refresh tokens from response - accessToken := res.Header.Get("X-auth-access-token") - refreshToken := res.Header.Get("X-auth-refresh-token") - if accessToken == "" || refreshToken == "" { - return "", "", fmt.Errorf("failed extracting access and refresh tokens from response") //nolint:goerr113 - } - return accessToken, refreshToken, nil -} - -type PagingResponse struct { - Offset int `json:"offset"` - Limit int `json:"limit"` - Count int `json:"count"` - Pages int `json:"pages"` -} - -type LinksResponse struct { - Self string `json:"self"` -} - -type APIResponse[T any] struct { - Links LinksResponse `json:"links"` - Paging PagingResponse `json:"paging"` - Items []T `json:"items"` -} - -type Domain struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Type string `json:"type"` -} - -type Device struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` -} - -// MakeRequest sends an HTTP request to the specified path using the given method and body. -// It retries the request with exponential backoff up to a maximum number of attempts. -// If the request fails after the maximum number of attempts, it returns an error. -// -// Parameters: -// - ctx: The context.Context for the request. -// - method: The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE). -// - path: The path of the resource to request. -// - body: The request body as an io.Reader. -// -// Returns: -// - *http.Response: The HTTP response if the request is successful. -// - error: An error if the request fails after the maximum number of attempts. -func (fmcc *fmcClient) MakeRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { - var ( - resp *http.Response - err error - ) - - for attempt := 0; attempt < maxRetries; attempt++ { - // Check if context is already canceled or expired - if ctx.Err() != nil { - return nil, ctx.Err() - } - - resp, err = fmcc.makeRequestOnce(ctx, method, path, body) - if err == nil { - return resp, nil - } - time.Sleep(exponentialBackoff(attempt)) - } - - return nil, fmt.Errorf("request failed after %d attempts: %w", maxRetries, err) -} - -// makeRequestOnce sends an HTTP request to the specified path using the given method and body. -// It is a helper function for MakeRequest that sends the request only once. -func (fmcc *fmcClient) makeRequestOnce(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { - ctxWithTimeout, cancel := context.WithTimeout(ctx, fmcc.DefaultTimeout) - defer cancel() - req, err := http.NewRequestWithContext(ctxWithTimeout, method, fmt.Sprintf("%s/%s", fmcc.BaseURL, path), body) - if err != nil { - return nil, err - } - // Set the Authorization header. - req.Header.Set("X-auth-access-token", fmcc.AccessToken) - return fmcc.HTTPClient.Do(req) -} - -// GetDomains returns a list of domains from the FMC API. -// It sends a GET request to the /fmc_platform/v1/info/domain endpoint. -func (fmcc *fmcClient) GetDomains() ([]Domain, error) { - offset := 0 - limit := 25 - domains := []Domain{} - ctx := context.Background() - defer ctx.Done() - for { - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, fmt.Sprintf("fmc_platform/v1/info/domain?offset=%d&limit=%d", offset, limit), nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - var marshaledResponse APIResponse[Domain] - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &marshaledResponse) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - if len(marshaledResponse.Items) > 0 { - domains = append(domains, marshaledResponse.Items...) - } - - if len(marshaledResponse.Items) < limit { - break - } - offset += limit - } - return domains, nil -} - -// GetDevices returns a list of devices from the FMC API for the specified domain. -func (fmcc *fmcClient) GetDevices(domainUUID string) ([]Device, error) { - offset := 0 - limit := 25 - devices := []Device{} - ctx := context.Background() - defer ctx.Done() - devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords?offset=%d&limit=%d", domainUUID, offset, limit) - for { - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - var marshaledResponse APIResponse[Device] - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &marshaledResponse) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - if len(marshaledResponse.Items) > 0 { - devices = append(devices, marshaledResponse.Items...) - } - - if len(marshaledResponse.Items) < limit { - break - } - offset += limit - } - return devices, nil -} - -type PhysicalInterface struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` -} - -// GetDevicePhysicalInterfaces returns a list of physical interfaces for the specified device in the specified domain. -func (fmcc *fmcClient) GetDevicePhysicalInterfaces(domainUUID string, deviceID string) ([]PhysicalInterface, error) { - offset := 0 - limit := 25 - pIfaces := []PhysicalInterface{} - ctx := context.Background() - defer ctx.Done() - pInterfacesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/physicalinterfaces?offset=%d&limit=%d", domainUUID, deviceID, offset, limit) - for { - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, pInterfacesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - var marshaledResponse APIResponse[PhysicalInterface] - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &marshaledResponse) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - if len(marshaledResponse.Items) > 0 { - pIfaces = append(pIfaces, marshaledResponse.Items...) - } - - if len(marshaledResponse.Items) < limit { - break - } - offset += limit - } - return pIfaces, nil -} - -type VlanInterface struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` -} - -func (fmcc *fmcClient) GetDeviceVLANInterfaces(domainUUID string, deviceID string) ([]VlanInterface, error) { - offset := 0 - limit := 25 - vlanIfaces := []VlanInterface{} - ctx := context.Background() - pInterfacesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/vlaninterfaces?offset=%d&limit=%d", domainUUID, deviceID, offset, limit) - for { - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, pInterfacesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - var marshaledResponse APIResponse[VlanInterface] - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &marshaledResponse) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - if len(marshaledResponse.Items) > 0 { - vlanIfaces = append(vlanIfaces, marshaledResponse.Items...) - } - - if len(marshaledResponse.Items) < limit { - break - } - offset += limit - } - return vlanIfaces, nil -} - -type PhysicalInterfaceInfo struct { - Type string `json:"type"` - MTU int `json:"MTU"` - Enabled bool `json:"enabled"` - Name string `json:"name"` - ID string `json:"id"` - Mode string `json:"mode"` - Description string `json:"description"` - Hardware *struct { - Speed string `json:"speed"` - Duplex string `json:"duplex"` - } `json:"hardware"` - SecurityZone *struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"securityZone"` - IPv4 *struct { - Static *struct { - Address string `json:"address"` - Netmask string `json:"netmask"` - } `json:"static"` - } `json:"ipv4"` - IPv6 *struct { - EnableIPv6 bool `json:"enableIPV6"` - } `json:"ipv6"` -} - -func (fmcc *fmcClient) GetPhysicalInterfaceInfo(domainUUID string, deviceID string, interfaceID string) (*PhysicalInterfaceInfo, error) { - var pInterfaceInfo PhysicalInterfaceInfo - ctx := context.Background() - defer ctx.Done() - devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/physicalinterfaces/%s", domainUUID, deviceID, interfaceID) - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &pInterfaceInfo) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - return &pInterfaceInfo, nil -} - -func (fmcc *fmcClient) GetVLANInterfaceInfo(domainUUID string, deviceID string, interfaceID string) (*VLANInterfaceInfo, error) { - var vlanInterfaceInfo VLANInterfaceInfo - ctx := context.Background() - defer ctx.Done() - devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s/vlaninterfaces/%s", domainUUID, deviceID, interfaceID) - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &vlanInterfaceInfo) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - return &vlanInterfaceInfo, nil -} - -// VLANInterfaceInfo represents information about a VLAN interface. -type VLANInterfaceInfo struct { - Type string `json:"type"` - Mode string `json:"mode"` - VID int `json:"vlanId"` - MTU int `json:"MTU"` - Enabled bool `json:"enabled"` - Name string `json:"name"` - ID string `json:"id"` - Description string `json:"description"` - Hardware *struct { - Speed string `json:"speed"` - Duplex string `json:"duplex"` - } `json:"hardware"` - SecurityZone *struct { - ID string `json:"id"` - Type string `json:"type"` - } `json:"securityZone"` - IPv4 *struct { - Static *struct { - Address string `json:"address"` - Netmask string `json:"netmask"` - } `json:"static"` - } `json:"ipv4"` - IPv6 *struct { - EnableIPv6 bool `json:"enableIPV6"` - } `json:"ipv6"` -} - -// DeviceInfo represents information about a FMC device. -type DeviceInfo struct { - Name string `json:"name"` - Description string `json:"description"` - Model string `json:"model"` - ModelID string `json:"modelId"` - ModelNumber string `json:"modelNumber"` - SWVersion string `json:"sw_version"` - Hostname string `json:"hostName"` - Metadata struct { - SerialNumber string `json:"deviceSerialNumber"` - InventoryData struct { - CPUCores string `json:"cpuCores"` - CPUType string `json:"cpuType"` - MemoryInMB string `json:"memoryInMB"` - } `json:"inventoryData"` - } `json:"metadata"` -} - -// GetDeviceInfo returns information about a device in the specified domain. -func (fmcc *fmcClient) GetDeviceInfo(domainUUID string, deviceID string) (*DeviceInfo, error) { - var deviceInfo DeviceInfo - ctx := context.Background() - devicesURL := fmt.Sprintf("fmc_config/v1/domain/%s/devices/devicerecords/%s", domainUUID, deviceID) - apiResponse, err := fmcc.MakeRequest(ctx, http.MethodGet, devicesURL, nil) - if err != nil { - return nil, fmt.Errorf("make request for domains: %w", err) - } - defer apiResponse.Body.Close() - if apiResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", apiResponse.StatusCode) - } - bodyBytes, err := io.ReadAll(apiResponse.Body) - if err != nil { - return nil, fmt.Errorf("response body readAll: %w", err) - } - err = json.Unmarshal(bodyBytes, &deviceInfo) - if err != nil { - return nil, fmt.Errorf("json unmarshal response body: %w", err) - } - - return &deviceInfo, nil -} diff --git a/internal/source/fmc/fmc_init.go b/internal/source/fmc/fmc_init.go index 46690a7..bad2233 100644 --- a/internal/source/fmc/fmc_init.go +++ b/internal/source/fmc/fmc_init.go @@ -1,9 +1,13 @@ package fmc -import "fmt" +import ( + "fmt" + + "github.com/bl4ko/netbox-ssot/internal/source/fmc/client" +) // Init initializes the FMC source. -func (fmcs *FMCSource) initObjects(c *fmcClient) error { +func (fmcs *FMCSource) initObjects(c *client.FMCClient) error { domains, err := fmcs.initDomains(c) if err != nil { return fmt.Errorf("init domains: %s", err) @@ -17,10 +21,9 @@ func (fmcs *FMCSource) initObjects(c *fmcClient) error { return nil } -func (fmcs *FMCSource) initDomains(c *fmcClient) ([]Domain, error) { - fmcs.Domains = make(map[string]Domain) - domains, err := c.GetDomains() +func (fmcs *FMCSource) initDomains(c *client.FMCClient) ([]client.Domain, error) { fmcs.Logger.Debug(fmcs.Ctx, "Getting domains from fmc...") + domains, err := c.GetDomains() if err != nil { return nil, fmt.Errorf("get domains: %s", err) } @@ -31,11 +34,7 @@ func (fmcs *FMCSource) initDomains(c *fmcClient) ([]Domain, error) { return domains, nil } -func (fmcs *FMCSource) initDevices(c *fmcClient, domain Domain) error { - fmcs.Devices = make(map[string]*DeviceInfo) - fmcs.DevicePhysicalIfaces = make(map[string][]*PhysicalInterfaceInfo) - fmcs.DeviceVlanIfaces = make(map[string][]*VLANInterfaceInfo) - +func (fmcs *FMCSource) initDevices(c *client.FMCClient, domain client.Domain) error { fmcs.Logger.Debugf(fmcs.Ctx, "Getting devices for %s domain...", domain.Name) devices, err := c.GetDevices(domain.UUID) if err != nil { @@ -67,8 +66,7 @@ func (fmcs *FMCSource) initDevices(c *fmcClient, domain Domain) error { return nil } -func (fmcs *FMCSource) initDevicePhysicalInterfaces(c *fmcClient, domain Domain, device Device) error { - fmcs.DevicePhysicalIfaces[device.ID] = make([]*PhysicalInterfaceInfo, 0) +func (fmcs *FMCSource) initDevicePhysicalInterfaces(c *client.FMCClient, domain client.Domain, device client.Device) error { pIfaces, err := c.GetDevicePhysicalInterfaces(domain.UUID, device.ID) if err != nil { return fmt.Errorf("error getting physical interfaces: %s", err) @@ -83,8 +81,7 @@ func (fmcs *FMCSource) initDevicePhysicalInterfaces(c *fmcClient, domain Domain, return nil } -func (fmcs *FMCSource) initDeviceVLANInterfaces(c *fmcClient, domain Domain, device Device) error { - fmcs.DeviceVlanIfaces[device.ID] = make([]*VLANInterfaceInfo, 0) +func (fmcs *FMCSource) initDeviceVLANInterfaces(c *client.FMCClient, domain client.Domain, device client.Device) error { vlanIfaces, err := c.GetDeviceVLANInterfaces(domain.UUID, device.ID) if err != nil { return fmt.Errorf("error getting vlan interfaces: %s", err)