Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions libs/grape/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,24 @@ 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

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 = ""
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
69 changes: 47 additions & 22 deletions libs/grape/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package grape
import (
"encoding/json"
"fmt"
"strings"
)

// getProvisioningServerID retrieves and caches the provisioning setting UUID.
Expand All @@ -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: <https://...>; rel="next", <https://...>; 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) {
Expand Down
4 changes: 2 additions & 2 deletions libs/grape/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion libs/grape/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
6 changes: 2 additions & 4 deletions providers/grape.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions providers/sraps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading