diff --git a/libs/grape/auth.go b/libs/grape/auth.go index 50fc69a..0c9f92d 100644 --- a/libs/grape/auth.go +++ b/libs/grape/auth.go @@ -130,8 +130,9 @@ func createHawkAuthHeader(creds *HawkCredentials, method, rawURL string, payload return authHeader, nil } -// makeHawkRequest performs an HTTP request with Hawk authentication -func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error) { +// makeHawkRequestFull performs an HTTP request with Hawk authentication and +// returns both the response body and headers (needed for Link-based pagination). +func (c *Client) makeHawkRequestFull(method, url string, body []byte) ([]byte, http.Header, error) { var req *http.Request var err error var contentType string @@ -139,14 +140,14 @@ func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error if body != nil { req, err = http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { - return nil, err + return nil, nil, err } contentType = "application/json" req.Header.Set("Content-Type", contentType) } else { req, err = http.NewRequest(method, url, nil) if err != nil { - return nil, err + return nil, nil, err } contentType = "" } @@ -162,7 +163,7 @@ func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error // Add Hawk authentication header authHeader, err := createHawkAuthHeader(creds, method, url, body, contentType) if err != nil { - return nil, fmt.Errorf("failed to create Hawk auth header: %v", err) + return nil, nil, fmt.Errorf("failed to create Hawk auth header: %v", err) } req.Header.Set("Authorization", authHeader) @@ -177,13 +178,13 @@ func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, err + return nil, nil, err } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } // Store response for debugging @@ -195,8 +196,14 @@ func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error } if resp.StatusCode >= 400 { - return nil, parseErrorResponse(resp.StatusCode, resp.Status, bodyBytes) + return nil, nil, parseErrorResponse(resp.StatusCode, resp.Status, bodyBytes) } - return bodyBytes, nil + return bodyBytes, resp.Header, nil +} + +// makeHawkRequest performs an HTTP request with Hawk authentication +func (c *Client) makeHawkRequest(method, url string, body []byte) ([]byte, error) { + bodyBytes, _, err := c.makeHawkRequestFull(method, url, body) + return bodyBytes, err } diff --git a/libs/grape/devices.go b/libs/grape/devices.go index 7a09b95..eae5b57 100644 --- a/libs/grape/devices.go +++ b/libs/grape/devices.go @@ -25,6 +25,7 @@ package grape import ( "encoding/json" "fmt" + "strings" ) // getProvisioningServerID retrieves and caches the provisioning setting UUID. @@ -33,39 +34,63 @@ import ( // Uses sync.Once to prevent duplicate API calls under concurrent load. func (c *Client) getProvisioningServerID() (string, error) { c.provisioningServerIDOnce.Do(func() { - // Get settings to find the provisioning setting UUID - settingsURL := c.BaseURL + "settings/" - settings, err := c.makeHawkRequest("GET", settingsURL, nil) - if err != nil { - c.provisioningServerIDErr = fmt.Errorf("failed to get settings: %w", err) - return - } + // Fetch all settings pages to find the provisioning setting UUID. + // The API paginates with default 20 items; use page_size=1000 to + // minimise round-trips. Pagination continues while the response + // contains a Link header with rel="next". + nextURL := c.BaseURL + "settings/?page_size=1000" + for nextURL != "" { + pageBytes, headers, err := c.makeHawkRequestFull("GET", nextURL, nil) + if err != nil { + c.provisioningServerIDErr = fmt.Errorf("failed to get settings: %w", err) + return + } - var settingsList []Setting - if err := json.Unmarshal(settings, &settingsList); err != nil { - c.provisioningServerIDErr = fmt.Errorf("failed to parse settings response: %w", err) - return - } + var page []Setting + if err := json.Unmarshal(pageBytes, &page); err != nil { + c.provisioningServerIDErr = fmt.Errorf("failed to parse settings response: %w", err) + return + } - var settingProvisioningServerUUID string - for _, setting := range settingsList { - if setting.ParamName == c.ProvisioningSettingName { - settingProvisioningServerUUID = setting.UUID - break + for _, setting := range page { + if setting.ParamName == c.ProvisioningSettingName { + c.provisioningServerID = setting.UUID + return + } } - } - if settingProvisioningServerUUID == "" { - c.provisioningServerIDErr = fmt.Errorf("%s setting not found in API response", c.ProvisioningSettingName) - return + // Follow Link header for next page if present + nextURL = parseLinkNext(headers.Get("Link")) } - c.provisioningServerID = settingProvisioningServerUUID + c.provisioningServerIDErr = fmt.Errorf("%s setting not found in API response", c.ProvisioningSettingName) }) return c.provisioningServerID, c.provisioningServerIDErr } +// parseLinkNext extracts the URL for rel="next" from an RFC 5988 Link header. +// Returns an empty string if there is no next page. +func parseLinkNext(linkHeader string) string { + // Format: ; rel="next", ; rel="prev" + for _, part := range strings.Split(linkHeader, ",") { + part = strings.TrimSpace(part) + segments := strings.Split(part, ";") + if len(segments) < 2 { + continue + } + for _, seg := range segments[1:] { + if strings.TrimSpace(seg) == `rel="next"` { + url := strings.TrimSpace(segments[0]) + url = strings.TrimPrefix(url, "<") + url = strings.TrimSuffix(url, ">") + return url + } + } + } + return "" +} + // getEndpointsURL retrieves and caches the endpoints URL for device operations // Uses sync.Once to prevent duplicate API calls under concurrent load func (c *Client) getEndpointsURL() (string, error) { diff --git a/libs/grape/errors.go b/libs/grape/errors.go index 66969ef..455805b 100644 --- a/libs/grape/errors.go +++ b/libs/grape/errors.go @@ -38,9 +38,9 @@ type APIError struct { // Error implements the error interface func (e APIError) Error() string { if e.Message != "" { - return fmt.Sprintf("GRAPE API error (HTTP %d): %s", e.StatusCode, e.Message) + return fmt.Sprintf("API error (HTTP %d): %s", e.StatusCode, e.Message) } - return fmt.Sprintf("GRAPE API error (HTTP %d): %s", e.StatusCode, e.Status) + return fmt.Sprintf("API error (HTTP %d): %s", e.StatusCode, e.Status) } // parseErrorResponse attempts to extract error information from the response body diff --git a/libs/grape/types.go b/libs/grape/types.go index bf9f14e..3ad92a4 100644 --- a/libs/grape/types.go +++ b/libs/grape/types.go @@ -46,6 +46,6 @@ type CompanyResponse struct { type DeviceData struct { MAC string `json:"mac"` AutoprovisioningEnabled bool `json:"autoprovisioning_enabled"` - WarrantyExpWarningPeriod *int `json:"warranty_exp_warning_period"` + WarrantyExpWarningPeriod *int `json:"warranty_exp_warning_period,omitempty"` SettingsManager map[string]map[string]interface{} `json:"settings_manager"` } diff --git a/providers/grape.go b/providers/grape.go index a706f05..97399e3 100644 --- a/providers/grape.go +++ b/providers/grape.go @@ -34,6 +34,7 @@ import ( "github.com/nethesis/falconieri/configuration" "github.com/nethesis/falconieri/libs/grape" "github.com/nethesis/falconieri/models" + "github.com/nethesis/falconieri/utils" ) var ( @@ -68,10 +69,7 @@ func (d GrapeDevice) Register() error { // Check if this is an API error (4xx/5xx from server) var apiErr grape.APIError if errors.As(err, &apiErr) { - return models.ProviderError{ - Message: "provider_remote_call_failed", - WrappedError: apiErr, - } + return utils.ParseProviderError(apiErr.Error()) } // Check if this is a transport-level error (DNS, connection, timeout) diff --git a/providers/sraps.go b/providers/sraps.go index d9f204c..3a5f113 100644 --- a/providers/sraps.go +++ b/providers/sraps.go @@ -35,6 +35,7 @@ import ( "github.com/nethesis/falconieri/configuration" "github.com/nethesis/falconieri/libs/grape" "github.com/nethesis/falconieri/models" + "github.com/nethesis/falconieri/utils" ) var ( @@ -69,10 +70,7 @@ func (d SrapsDevice) Register() error { if err != nil { var apiErr grape.APIError if errors.As(err, &apiErr) { - return models.ProviderError{ - Message: "provider_remote_call_failed", - WrappedError: apiErr, - } + return utils.ParseProviderError(apiErr.Error()) } var urlErr *url.Error diff --git a/utils/utils.go b/utils/utils.go index 98d5da9..1ba2f8d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -79,7 +79,9 @@ func ParseProviderError(message string) error { message == "800004", //ymcs message == "800003", //ymcs: Resource already exists strings.Contains(strings.ToLower(message), "device already managed"), - strings.Contains(strings.ToLower(message), "resource already exists"): + strings.Contains(strings.ToLower(message), "resource already exists"), + strings.Contains(strings.ToLower(message), "taken_mac_or_forbidden"), //grape/sraps + strings.Contains(strings.ToLower(message), "permission") && strings.Contains(strings.ToLower(message), "endpoint"): //grape/sraps: "User lacks permission to endpoint" return errors.New("device_owned_by_other_user")