2023-04-10 15:36:59 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2022-02-04 05:01:59 +00:00
package fingerprint
import (
2022-02-07 16:48:42 +00:00
"fmt"
2023-03-08 19:25:10 +00:00
"io"
2022-02-04 05:01:59 +00:00
"net/http"
"net/url"
"os"
"strings"
"time"
cleanhttp "github.com/hashicorp/go-cleanhttp"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/helper/useragent"
"github.com/hashicorp/nomad/nomad/structs"
)
const (
2022-02-06 06:23:43 +00:00
// DigitalOceanMetadataURL is where the DigitalOcean metadata api normally resides.
2022-02-07 16:48:42 +00:00
// https://docs.digitalocean.com/products/droplets/how-to/retrieve-droplet-metadata/#how-to-retrieve-droplet-metadata
2022-02-04 05:01:59 +00:00
DigitalOceanMetadataURL = "http://169.254.169.254/metadata/v1/"
// DigitalOceanMetadataTimeout is the timeout used when contacting the DigitalOcean metadata
// services.
DigitalOceanMetadataTimeout = 2 * time . Second
)
type DigitalOceanMetadataPair struct {
path string
unique bool
}
// EnvDigitalOceanFingerprint is used to fingerprint DigitalOcean metadata
type EnvDigitalOceanFingerprint struct {
StaticFingerprinter
client * http . Client
logger log . Logger
metadataURL string
}
// NewEnvDigitalOceanFingerprint is used to create a fingerprint from DigitalOcean metadata
func NewEnvDigitalOceanFingerprint ( logger log . Logger ) Fingerprint {
// Read the internal metadata URL from the environment, allowing test files to
// provide their own
metadataURL := os . Getenv ( "DO_ENV_URL" )
if metadataURL == "" {
metadataURL = DigitalOceanMetadataURL
}
// assume 2 seconds is enough time for inside DigitalOcean network
client := & http . Client {
Timeout : DigitalOceanMetadataTimeout ,
Transport : cleanhttp . DefaultTransport ( ) ,
}
return & EnvDigitalOceanFingerprint {
client : client ,
logger : logger . Named ( "env_digitalocean" ) ,
metadataURL : metadataURL ,
}
}
func ( f * EnvDigitalOceanFingerprint ) Get ( attribute string , format string ) ( string , error ) {
reqURL := f . metadataURL + attribute
parsedURL , err := url . Parse ( reqURL )
if err != nil {
return "" , err
}
req := & http . Request {
2022-02-07 16:48:42 +00:00
Method : http . MethodGet ,
2022-02-04 05:01:59 +00:00
URL : parsedURL ,
Header : http . Header {
"User-Agent" : [ ] string { useragent . String ( ) } ,
} ,
}
res , err := f . client . Do ( req )
if err != nil {
2022-02-07 16:48:42 +00:00
f . logger . Debug ( "failed to request metadata" , "attribute" , attribute , "error" , err )
2022-02-04 05:01:59 +00:00
return "" , err
}
2023-03-08 19:25:10 +00:00
body , err := io . ReadAll ( res . Body )
2022-02-04 05:01:59 +00:00
res . Body . Close ( )
if err != nil {
2022-02-07 16:48:42 +00:00
f . logger . Error ( "failed to read metadata" , "attribute" , attribute , "error" , err , "resp_code" , res . StatusCode )
2022-02-04 05:01:59 +00:00
return "" , err
}
2022-02-07 16:48:42 +00:00
if res . StatusCode != http . StatusOK {
f . logger . Debug ( "could not read value for attribute" , "attribute" , attribute , "resp_code" , res . StatusCode )
return "" , fmt . Errorf ( "error reading attribute %s. digitalocean metadata api returned an error: resp_code: %d, resp_body: %s" , attribute , res . StatusCode , body )
2022-02-04 05:01:59 +00:00
}
2022-02-07 16:48:42 +00:00
return string ( body ) , nil
2022-02-04 05:01:59 +00:00
}
func ( f * EnvDigitalOceanFingerprint ) Fingerprint ( request * FingerprintRequest , response * FingerprintResponse ) error {
cfg := request . Config
// Check if we should tighten the timeout
if cfg . ReadBoolDefault ( TightenNetworkTimeoutsConfig , false ) {
f . client . Timeout = 1 * time . Millisecond
}
if ! f . isDigitalOcean ( ) {
return nil
}
// Keys and whether they should be namespaced as unique. Any key whose value
// uniquely identifies a node, such as ip, should be marked as unique. When
// marked as unique, the key isn't included in the computed node class.
keys := map [ string ] DigitalOceanMetadataPair {
"id" : { unique : true , path : "id" } ,
"hostname" : { unique : true , path : "hostname" } ,
"region" : { unique : false , path : "region" } ,
"private-ipv4" : { unique : true , path : "interfaces/private/0/ipv4/address" } ,
"public-ipv4" : { unique : true , path : "interfaces/public/0/ipv4/address" } ,
"private-ipv6" : { unique : true , path : "interfaces/private/0/ipv6/address" } ,
"public-ipv6" : { unique : true , path : "interfaces/public/0/ipv6/address" } ,
"mac" : { unique : true , path : "interfaces/public/0/mac" } ,
}
for k , attr := range keys {
resp , err := f . Get ( attr . path , "text" )
v := strings . TrimSpace ( resp )
if err != nil {
2022-09-01 13:06:10 +00:00
f . logger . Warn ( "failed to read attribute" , "attribute" , k , "error" , err )
2022-02-07 16:48:42 +00:00
continue
2022-02-04 05:01:59 +00:00
} else if v == "" {
f . logger . Debug ( "read an empty value" , "attribute" , k )
continue
}
// assume we want blank entries
key := "platform.digitalocean." + strings . ReplaceAll ( k , "/" , "." )
if attr . unique {
key = structs . UniqueNamespace ( key )
}
response . AddAttribute ( key , v )
}
// copy over network specific information
if val , ok := response . Attributes [ "unique.platform.digitalocean.local-ipv4" ] ; ok && val != "" {
response . AddAttribute ( "unique.network.ip-address" , val )
}
// populate Links
if id , ok := response . Attributes [ "unique.platform.digitalocean.id" ] ; ok {
response . AddLink ( "digitalocean" , id )
}
response . Detected = true
return nil
}
func ( f * EnvDigitalOceanFingerprint ) isDigitalOcean ( ) bool {
v , err := f . Get ( "region" , "text" )
v = strings . TrimSpace ( v )
return err == nil && v != ""
}