-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
client.go
325 lines (269 loc) · 11 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package bigipreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/bigipreceiver"
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"go.opentelemetry.io/collector/component"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/bigipreceiver/internal/models"
)
const (
// loginPath is the path to the login endpoint
loginPath = "/mgmt/shared/authn/login"
// virtualServersPath is the path to the virtual servers endpoint
virtualServersPath = "/mgmt/tm/ltm/virtual"
// virtualServersStatsPath is the path to the virtual servers statistics endpoint
virtualServersStatsPath = "/mgmt/tm/ltm/virtual/stats"
// poolsStatsPath is the path to the pools statistics endpoint
poolsStatsPath = "/mgmt/tm/ltm/pool/stats"
// nodesStatsPath is the path to the nodes statistics endpoint
nodesStatsPath = "/mgmt/tm/ltm/node/stats"
// poolMembersStatsPathSuffix is the suffix added onto an individual pool's statistics endpoint
poolMembersStatsPathSuffix = "/members/stats"
)
// custom errors
var (
errCollectedNoPoolMembers = errors.New(`all pool member requests have failed`)
)
// client is used for retrieving data about a Big-IP environment
type client interface {
// HasToken checks if the client currently has an auth token
HasToken() bool
// GetNewToken must be called initially as it retrieves and sets an auth token for future calls
GetNewToken(ctx context.Context) error
// GetVirtualServers retrieves data for all LTM virtual servers in a Big-IP environment
GetVirtualServers(ctx context.Context) (*models.VirtualServers, error)
// GetPools retrieves data for all LTM pools in a Big-IP environment
GetPools(ctx context.Context) (*models.Pools, error)
// GetPoolMembers retrieves data for all LTM pool members in a Big-IP environment
GetPoolMembers(ctx context.Context, pools *models.Pools) (*models.PoolMembers, error)
// GetNodes retrieves data for all LTM nodes in a Big-IP environment
GetNodes(ctx context.Context) (*models.Nodes, error)
}
// bigipClient implements the client interface and retrieves data through the iControl REST API
type bigipClient struct {
client *http.Client
hostEndpoint string
creds bigipCredentials
token string
logger *zap.Logger
}
// bigipCredentials stores the username and password needed to retrieve an access token from the iControl REST API
type bigipCredentials struct {
username string
password string
}
// Verify bigipClient implements client interface
var _ client = (*bigipClient)(nil)
// newClient creates an initialized client (but with no token)
func newClient(ctx context.Context, cfg *Config, host component.Host, settings component.TelemetrySettings, logger *zap.Logger) (client, error) {
httpClient, err := cfg.ToClient(ctx, host, settings)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP Client: %w", err)
}
return &bigipClient{
client: httpClient,
hostEndpoint: cfg.Endpoint,
creds: bigipCredentials{
username: cfg.Username,
password: string(cfg.Password),
},
logger: logger,
}, nil
}
// HasToken checks to see if an auth token has been set for the client
func (c *bigipClient) HasToken() bool {
return c.token != ""
}
// GetNewToken makes an appropriate call to the iControl REST login endpoint and sets the returned token on the bigipClient
func (c *bigipClient) GetNewToken(ctx context.Context) error {
var tokenDetails *models.TokenDetails
if err := c.post(ctx, loginPath, &tokenDetails); err != nil {
c.logger.Debug("Failed to retrieve api token", zap.Error(err))
return err
}
c.token = tokenDetails.Token.Token
return nil
}
// GetVirtualServers makes calls to both the standard and statistics version of the virtual servers endpoint.
// It combines this info into one object and returns it.
func (c *bigipClient) GetVirtualServers(ctx context.Context) (*models.VirtualServers, error) {
// get standard Virtual Server details
var virtualServers *models.VirtualServers
if err := c.get(ctx, virtualServersStatsPath, &virtualServers); err != nil {
c.logger.Debug("Failed to retrieve virtual servers", zap.Error(err))
return nil, err
}
// get statistic virtual server details and combine them
var virtualServersDetails *models.VirtualServersDetails
if err := c.get(ctx, virtualServersPath, &virtualServersDetails); err != nil {
c.logger.Warn("Failed to retrieve virtual servers properties", zap.Error(err))
return virtualServers, nil
}
return addVirtualServerPoolDetails(virtualServers, virtualServersDetails), nil
}
// GetPools makes a call the statistics version of the pools endpoint and returns the data.
func (c *bigipClient) GetPools(ctx context.Context) (*models.Pools, error) {
var pools *models.Pools
if err := c.get(ctx, poolsStatsPath, &pools); err != nil {
c.logger.Debug("Failed to retrieve pools", zap.Error(err))
return nil, err
}
return pools, nil
}
// GetPoolMembers takes in a list of all Pool data. It then iterates over this list to make a call to the statistics version
// of each pool's pool members endpoint. It accumulates all of this data into a single pool members object and returns it.
func (c *bigipClient) GetPoolMembers(ctx context.Context, pools *models.Pools) (*models.PoolMembers, error) {
var (
poolMembers *models.PoolMembers
combinedPoolMembers *models.PoolMembers
)
collectedPoolMembers := false
var errors []error
// for each pool get pool member info and aggregate it into a single spot
for poolURL := range pools.Entries {
poolMemberPath := strings.TrimPrefix(poolURL, "https://localhost")
poolMemberPath = strings.TrimSuffix(poolMemberPath, "/stats") + poolMembersStatsPathSuffix
if err := c.get(ctx, poolMemberPath, &poolMembers); err != nil {
errors = append(errors, err)
c.logger.Warn("Failed to retrieve all pool members", zap.Error(err))
} else {
combinedPoolMembers = combinePoolMembers(combinedPoolMembers, poolMembers)
collectedPoolMembers = true
}
}
combinedErr := multierr.Combine(errors...)
if combinedErr != nil && !collectedPoolMembers {
return nil, errCollectedNoPoolMembers
}
return combinedPoolMembers, combinedErr
}
// GetNodes makes a call the statistics version of the nodes endpoint and returns the data.
func (c *bigipClient) GetNodes(ctx context.Context) (nodes *models.Nodes, err error) {
if err = c.get(ctx, nodesStatsPath, &nodes); err != nil {
c.logger.Debug("Failed to retrieve nodes", zap.Error(err))
return nil, err
}
return nodes, nil
}
// post makes a POST request for the passed in path and stores result in the respObj
func (c *bigipClient) post(ctx context.Context, path string, respObj any) error {
// Construct endpoint and create request
url := c.hostEndpoint + path
postBody, _ := json.Marshal(map[string]string{
"username": c.creds.username,
"password": c.creds.password,
"loginProviderName": "tmos",
})
requestBody := bytes.NewBuffer(postBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)
if err != nil {
return fmt.Errorf("failed to create post request for path %s: %w", path, err)
}
return c.makeHTTPRequest(req, respObj)
}
// get makes a GET request (with token in header) for the passed in path and stores result in the respObj
func (c *bigipClient) get(ctx context.Context, path string, respObj any) error {
// Construct endpoint and create request
url := c.hostEndpoint + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
req.Header.Add("X-F5-Auth-Token", c.token)
if err != nil {
return fmt.Errorf("failed to create get request for path %s: %w", path, err)
}
return c.makeHTTPRequest(req, respObj)
}
// makeHTTPRequest makes the request and decodes the body into the respObj on a 200 Status
func (c *bigipClient) makeHTTPRequest(req *http.Request, respObj any) (err error) {
// Make request
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("failed to make http request: %w", err)
}
// Defer body close
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("failed to close response body", zap.Error(closeErr))
}
}()
// Check for OK status code
if err = c.checkHTTPStatus(resp); err != nil {
return err
}
// Decode the payload into the passed in response object
if err := json.NewDecoder(resp.Body).Decode(respObj); err != nil {
return fmt.Errorf("failed to decode response payload: %w", err)
}
return nil
}
// checkHTTPStatus returns an error if the response status is != 200
func (c *bigipClient) checkHTTPStatus(resp *http.Response) (err error) {
if resp.StatusCode != http.StatusOK {
c.logger.Debug("Big-IP API non-200", zap.Error(err), zap.Int("status_code", resp.StatusCode))
// Attempt to extract the error payload
payloadData, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Debug("failed to read payload error message", zap.Error(err))
} else {
c.logger.Debug("Big-IP API Error", zap.ByteString("api_error", payloadData))
}
return fmt.Errorf("non 200 code returned %d", resp.StatusCode)
}
return nil
}
// combinePoolMembers takes two PoolMembers and returns an aggregate of them both
func combinePoolMembers(poolMembersA *models.PoolMembers, poolMembersB *models.PoolMembers) *models.PoolMembers {
var aSize int
if poolMembersA != nil {
aSize = len(poolMembersA.Entries)
}
var bSize int
if poolMembersB != nil {
bSize = len(poolMembersB.Entries)
}
totalSize := aSize + bSize
if totalSize == 0 {
return &models.PoolMembers{}
}
combinedPoolMembers := models.PoolMembers{Entries: make(map[string]models.PoolMemberStats, totalSize)}
if poolMembersA != nil {
for url, data := range poolMembersA.Entries {
combinedPoolMembers.Entries[url] = data
}
}
if poolMembersB != nil {
for url, data := range poolMembersB.Entries {
combinedPoolMembers.Entries[url] = data
}
}
return &combinedPoolMembers
}
// addVirtualServerPoolDetails takes in VirtualServers and VirtualServersDetails, matches the data, and combines them into a returned VirtualServers
func addVirtualServerPoolDetails(virtualServers *models.VirtualServers, virtualServersDetails *models.VirtualServersDetails) *models.VirtualServers {
vSize := len(virtualServers.Entries)
if vSize == 0 {
return &models.VirtualServers{}
}
combinedVirtualServers := models.VirtualServers{Entries: make(map[string]models.VirtualServerStats, vSize)}
for virtualServerURL, entry := range virtualServers.Entries {
combinedVirtualServers.Entries[virtualServerURL] = entry
}
// for each item in VirtualServersDetails match it with the entry in VirtualServers, combine it, and add it to the combined data object
for _, item := range virtualServersDetails.Items {
parts := strings.Split(item.SelfLink, "?")
entryKey := parts[0] + "/stats"
if entryValue, ok := combinedVirtualServers.Entries[entryKey]; ok {
entryValue.NestedStats.Entries.PoolName.Description = item.PoolName
combinedVirtualServers.Entries[entryKey] = entryValue
}
}
return &combinedVirtualServers
}