122 lines
2.6 KiB
Go
122 lines
2.6 KiB
Go
package utilization
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maxResponseLengthBytes = 255
|
|
|
|
// AWS data gathering requires making three web requests, therefore this
|
|
// timeout is in keeping with the spec's total timeout of 1 second.
|
|
individualConnectionTimeout = 300 * time.Millisecond
|
|
)
|
|
|
|
const (
|
|
awsHost = "169.254.169.254"
|
|
|
|
typeEndpointPath = "/2008-02-01/meta-data/instance-type"
|
|
idEndpointPath = "/2008-02-01/meta-data/instance-id"
|
|
zoneEndpointPath = "/2008-02-01/meta-data/placement/availability-zone"
|
|
|
|
typeEndpoint = "http://" + awsHost + typeEndpointPath
|
|
idEndpoint = "http://" + awsHost + idEndpointPath
|
|
zoneEndpoint = "http://" + awsHost + zoneEndpointPath
|
|
)
|
|
|
|
// awsValidationError represents a response from an AWS endpoint that doesn't
|
|
// match the format expectations.
|
|
type awsValidationError struct {
|
|
e error
|
|
}
|
|
|
|
func (a awsValidationError) Error() string {
|
|
return a.e.Error()
|
|
}
|
|
|
|
func isAWSValidationError(e error) bool {
|
|
_, is := e.(awsValidationError)
|
|
return is
|
|
}
|
|
|
|
func getAWS() (*vendor, error) {
|
|
return getEndpoints(&http.Client{
|
|
Timeout: individualConnectionTimeout,
|
|
})
|
|
}
|
|
|
|
func getEndpoints(client *http.Client) (*vendor, error) {
|
|
v := &vendor{}
|
|
var err error
|
|
|
|
v.ID, err = getAndValidate(client, idEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v.Type, err = getAndValidate(client, typeEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v.Zone, err = getAndValidate(client, zoneEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func getAndValidate(client *http.Client, endpoint string) (string, error) {
|
|
response, err := client.Get(endpoint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != 200 {
|
|
return "", fmt.Errorf("unexpected response code %d", response.StatusCode)
|
|
}
|
|
|
|
b := make([]byte, maxResponseLengthBytes+1)
|
|
num, err := response.Body.Read(b)
|
|
if err != nil && err != io.EOF {
|
|
return "", err
|
|
}
|
|
|
|
if num > maxResponseLengthBytes {
|
|
return "", awsValidationError{
|
|
fmt.Errorf("maximum length %d exceeded", maxResponseLengthBytes),
|
|
}
|
|
}
|
|
|
|
responseText := string(b[:num])
|
|
|
|
for _, r := range responseText {
|
|
if !isAcceptableRune(r) {
|
|
return "", awsValidationError{
|
|
fmt.Errorf("invalid character %x", r),
|
|
}
|
|
}
|
|
}
|
|
|
|
return responseText, nil
|
|
}
|
|
|
|
// See:
|
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md#normalizing-aws-data
|
|
func isAcceptableRune(r rune) bool {
|
|
switch r {
|
|
case 0xFFFD:
|
|
return false
|
|
case '_', ' ', '/', '.', '-':
|
|
return true
|
|
default:
|
|
return r > 0x7f ||
|
|
('0' <= r && r <= '9') ||
|
|
('a' <= r && r <= 'z') ||
|
|
('A' <= r && r <= 'Z')
|
|
}
|
|
}
|