master
go 253 lines 7.25 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package cato_networks
4
5 import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "net/http"
13 "strings"
14
15 catosdk "github.com/catonetworks/cato-go-sdk"
16 catomodels "github.com/catonetworks/cato-go-sdk/models"
17 )
18
19 type apiClient interface {
20 Probe(ctx context.Context, accountID string) error
21 LookupSites(ctx context.Context, accountID string, limit, from int64) (*catosdk.EntityLookup, error)
22 AccountSnapshot(ctx context.Context, accountID string, siteIDs []string) (*catosdk.AccountSnapshot, error)
23 AccountMetrics(ctx context.Context, accountID string, siteIDs []string, timeFrame string, buckets int64, groupInterfaces *bool) (*catosdk.AccountMetrics, error)
24 SiteBgpStatus(ctx context.Context, accountID, siteID string) ([]*catosdk.SiteBgpStatusResult, error)
25 }
26
27 type sdkAPIClient struct {
28 client *catosdk.Client
29 raw rawGraphQLClient
30 }
31
32 type rawGraphQLClient struct {
33 url string
34 apiKey string
35 headers map[string]string
36 httpClient *http.Client
37 }
38
39 func newSDKAPIClient(cfg Config, httpClient *http.Client) (apiClient, error) {
40 headers := catoRequestHeaders(cfg.Headers)
41
42 client, err := catosdk.New(cfg.URL, cfg.APIKey, cfg.AccountID, httpClient, headers)
43 if err != nil {
44 return nil, err
45 }
46
47 return &sdkAPIClient{
48 client: client,
49 raw: rawGraphQLClient{
50 url: cfg.URL,
51 apiKey: cfg.APIKey,
52 headers: headers,
53 httpClient: httpClient,
54 },
55 }, nil
56 }
57
58 func catoRequestHeaders(src map[string]string) map[string]string {
59 headers := make(map[string]string, len(src)+1)
60 for key, value := range src {
61 if isCatoReservedHeader(key) {
62 continue
63 }
64 headers[key] = value
65 }
66 if !hasCatoHeader(headers, "User-Agent") {
67 headers["User-Agent"] = "Netdata go.d.plugin cato_networks"
68 }
69 return headers
70 }
71
72 func hasCatoHeader(headers map[string]string, key string) bool {
73 want := http.CanonicalHeaderKey(strings.TrimSpace(key))
74 for existing := range headers {
75 if http.CanonicalHeaderKey(strings.TrimSpace(existing)) == want {
76 return true
77 }
78 }
79 return false
80 }
81
82 func (c *sdkAPIClient) Probe(ctx context.Context, accountID string) error {
83 limit := int64(1)
84 from := int64(0)
85 _, err := c.client.EntityLookup(ctx, accountID, catomodels.EntityTypeSite, &limit, &from, nil, nil, nil, nil, nil, nil)
86 if err != nil {
87 return fmt.Errorf("entityLookup: %w", err)
88 }
89 return nil
90 }
91
92 func (c *sdkAPIClient) LookupSites(ctx context.Context, accountID string, limit, from int64) (*catosdk.EntityLookup, error) {
93 res, err := c.client.EntityLookup(ctx, accountID, catomodels.EntityTypeSite, &limit, &from, nil, nil, nil, nil, nil, nil)
94 if err != nil {
95 return nil, fmt.Errorf("entityLookup: %w", err)
96 }
97 return res, nil
98 }
99
100 func (c *sdkAPIClient) AccountSnapshot(ctx context.Context, accountID string, siteIDs []string) (*catosdk.AccountSnapshot, error) {
101 res, err := c.client.AccountSnapshot(ctx, siteIDs, nil, &accountID)
102 if err != nil && isAccountSnapshotEnumDecodeError(err) {
103 res, err = c.raw.AccountSnapshot(ctx, accountID, siteIDs)
104 }
105 if err != nil {
106 return nil, fmt.Errorf("accountSnapshot: %w", err)
107 }
108 return res, nil
109 }
110
111 func (c *sdkAPIClient) AccountMetrics(ctx context.Context, accountID string, siteIDs []string, timeFrame string, buckets int64, groupInterfaces *bool) (*catosdk.AccountMetrics, error) {
112 labels := []catomodels.TimeseriesMetricType{
113 catomodels.TimeseriesMetricTypeBytesUpstreamMax,
114 catomodels.TimeseriesMetricTypeBytesDownstreamMax,
115 catomodels.TimeseriesMetricTypeLostUpstreamPcnt,
116 catomodels.TimeseriesMetricTypeLostDownstreamPcnt,
117 catomodels.TimeseriesMetricTypeJitterUpstream,
118 catomodels.TimeseriesMetricTypeJitterDownstream,
119 catomodels.TimeseriesMetricTypePacketsDiscardedUpstream,
120 catomodels.TimeseriesMetricTypePacketsDiscardedDownstream,
121 catomodels.TimeseriesMetricTypeRtt,
122 catomodels.TimeseriesMetricTypeLastMileLatency,
123 catomodels.TimeseriesMetricTypeLastMilePacketLoss,
124 }
125
126 res, err := c.client.AccountMetrics(ctx,
127 nil, nil, nil, &buckets,
128 labels,
129 nil, nil, nil, nil, nil, nil, nil, nil,
130 siteIDs,
131 nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
132 nil, nil, &accountID, nil, timeFrame, groupInterfaces, nil,
133 )
134 if err != nil {
135 return nil, fmt.Errorf("accountMetrics: %w", err)
136 }
137 return res, nil
138 }
139
140 func (c *sdkAPIClient) SiteBgpStatus(ctx context.Context, accountID, siteID string) ([]*catosdk.SiteBgpStatusResult, error) {
141 input := catomodels.SiteBgpStatusInput{
142 Site: &catomodels.SiteRefInput{
143 By: catomodels.ObjectRefByID,
144 Input: siteID,
145 },
146 }
147
148 _, res, err := c.client.SiteBgpStatus(ctx, input, accountID)
149 if err != nil {
150 return nil, fmt.Errorf("siteBgpStatus: %w", err)
151 }
152 return res, nil
153 }
154
155 type rawGraphQLRequest struct {
156 OperationName string `json:"operationName,omitempty"`
157 Query string `json:"query"`
158 Variables map[string]any `json:"variables,omitempty"`
159 }
160
161 type rawGraphQLError struct {
162 Message string `json:"message"`
163 }
164
165 type rawAccountSnapshotResponse struct {
166 Data catosdk.AccountSnapshot `json:"data"`
167 Errors []rawGraphQLError `json:"errors"`
168 NetworkErrors []rawGraphQLError `json:"networkErrors"`
169 GraphQLErrors []rawGraphQLError `json:"graphqlErrors"`
170 }
171
172 func (c rawGraphQLClient) AccountSnapshot(ctx context.Context, accountID string, siteIDs []string) (*catosdk.AccountSnapshot, error) {
173 payload := rawGraphQLRequest{
174 OperationName: "accountSnapshot",
175 Query: catosdk.AccountSnapshotDocument,
176 Variables: map[string]any{
177 "siteIDs": siteIDs,
178 "userIDs": nil,
179 "accountID": accountID,
180 },
181 }
182
183 body, err := json.Marshal(payload)
184 if err != nil {
185 return nil, err
186 }
187
188 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
189 if err != nil {
190 return nil, err
191 }
192 for key, value := range c.headers {
193 if isCatoReservedHeader(key) {
194 continue
195 }
196 req.Header.Set(key, value)
197 }
198 req.Header.Set("Content-Type", "application/json")
199 req.Header.Set("x-api-key", c.apiKey)
200 req.Header.Set("x-account-id", accountID)
201
202 resp, err := c.httpClient.Do(req)
203 if err != nil {
204 return nil, err
205 }
206 defer resp.Body.Close()
207
208 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
209 _, _ = io.Copy(io.Discard, resp.Body)
210 return nil, fmt.Errorf("http status %d", resp.StatusCode)
211 }
212
213 var decoded rawAccountSnapshotResponse
214 if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
215 return nil, err
216 }
217 if msg := firstRawGraphQLError(decoded.Errors, decoded.NetworkErrors, decoded.GraphQLErrors); msg != "" {
218 return nil, fmt.Errorf("graphql: %s", msg)
219 }
220 if decoded.Data.AccountSnapshot == nil {
221 return nil, errors.New("graphql accountSnapshot returned no data")
222 }
223
224 return &decoded.Data, nil
225 }
226
227 func firstRawGraphQLError(groups ...[]rawGraphQLError) string {
228 for _, group := range groups {
229 for _, err := range group {
230 if msg := strings.TrimSpace(err.Message); msg != "" {
231 return msg
232 }
233 }
234 }
235 return ""
236 }
237
238 func isCatoReservedHeader(key string) bool {
239 switch http.CanonicalHeaderKey(strings.TrimSpace(key)) {
240 case "Content-Type", "X-Api-Key", "X-Account-Id":
241 return true
242 default:
243 return false
244 }
245 }
246
247 func isAccountSnapshotEnumDecodeError(err error) bool {
248 if err == nil {
249 return false
250 }
251 msg := err.Error()
252 return strings.Contains(msg, "ConnectivityStatus") && strings.Contains(msg, "not a valid")
253 }