client.go 11 KB

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