lib/promscrape/discovery/openstack: code prettifying after cbe3cf683b

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/728
This commit is contained in:
Aliaksandr Valialkin 2020-10-05 18:11:51 +03:00
parent 991fad7855
commit aba899c298
6 changed files with 110 additions and 177 deletions

View file

@ -16,27 +16,22 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
const authHeaderName = "X-Auth-Token" // for making requests to openstack api
var configMap = discoveryutils.NewConfigMap() var configMap = discoveryutils.NewConfigMap()
// apiCredentials can be refreshed // apiCredentials can be refreshed
type apiCredentials struct { type apiCredentials struct {
// computeURL obtained from auth response and maybe changed
computeURL *url.URL computeURL *url.URL
// value of authHeaderName
token string token string
expiration time.Time expiration time.Time
} }
type apiConfig struct { type apiConfig struct {
// client may use tls
client *http.Client client *http.Client
port int port int
// tokenLock - guards creds refresh // tokenLock guards creds refresh
tokenLock sync.Mutex tokenLock sync.Mutex
creds *apiCredentials creds *apiCredentials
// authTokenReq - request for apiCredentials // authTokenReq contins request body for apiCredentials
authTokenReq []byte authTokenReq []byte
// keystone endpoint // keystone endpoint
endpoint *url.URL endpoint *url.URL
@ -50,19 +45,17 @@ func (cfg *apiConfig) getFreshAPICredentials() (*apiCredentials, error) {
cfg.tokenLock.Lock() cfg.tokenLock.Lock()
defer cfg.tokenLock.Unlock() defer cfg.tokenLock.Unlock()
if time.Until(cfg.creds.expiration) > 10*time.Second { if cfg.creds != nil && time.Until(cfg.creds.expiration) > 10*time.Second {
// Credentials aren't expired yet. // Credentials aren't expired yet.
return cfg.creds, nil return cfg.creds, nil
} }
newCreds, err := getCreds(cfg) newCreds, err := getCreds(cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed token refresh: %w", err) return nil, fmt.Errorf("cannot refresh OpenStack api token: %w", err)
} }
logger.Infof("refreshed, next : %v", cfg.creds.expiration.String())
cfg.creds = newCreds cfg.creds = newCreds
logger.Infof("successfully refreshed OpenStack api token; next expiration: %s", newCreds.expiration)
return cfg.creds, nil return newCreds, nil
} }
func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
@ -75,22 +68,20 @@ func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
cfg := &apiConfig{ cfg := &apiConfig{
client: discoveryutils.GetHTTPClient(), client: &http.Client{},
availability: sdc.Availability, availability: sdc.Availability,
region: sdc.Region, region: sdc.Region,
allTenants: sdc.AllTenants, allTenants: sdc.AllTenants,
port: sdc.Port, port: sdc.Port,
} }
if sdc.TLSConfig != nil { if sdc.TLSConfig != nil {
config, err := promauth.NewConfig(baseDir, nil, "", "", sdc.TLSConfig) config, err := promauth.NewConfig(baseDir, nil, "", "", sdc.TLSConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tr := &http.Transport{ cfg.client.Transport = &http.Transport{
TLSClientConfig: config.NewTLSConfig(), TLSClientConfig: config.NewTLSConfig(),
} }
cfg.client.Transport = tr
} }
// use public compute endpoint by default // use public compute endpoint by default
if len(cfg.availability) == 0 { if len(cfg.availability) == 0 {
@ -100,7 +91,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
// create new variable to prevent side effects // create new variable to prevent side effects
sdcAuth := *sdc sdcAuth := *sdc
// special case if identity_endpoint is not defined // special case if identity_endpoint is not defined
if len(sdc.IdentityEndpoint) == 0 { if len(sdcAuth.IdentityEndpoint) == 0 {
// override sdc // override sdc
sdcAuth = readCredentialsFromEnv() sdcAuth = readCredentialsFromEnv()
} }
@ -115,19 +106,15 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
return nil, err return nil, err
} }
cfg.authTokenReq = tokenReq cfg.authTokenReq = tokenReq
token, err := getCreds(cfg) // cfg.creds is populated at getFreshAPICredentials
if err != nil {
return nil, err
}
cfg.creds = token
return cfg, nil return cfg, nil
} }
// getCreds - make call to openstack keystone api and retrieves token and computeURL // getCreds makes a call to openstack keystone api and retrieves token and computeURL
// https://docs.openstack.org/api-ref/identity/v3/ //
// See https://docs.openstack.org/api-ref/identity/v3/
func getCreds(cfg *apiConfig) (*apiCredentials, error) { func getCreds(cfg *apiConfig) (*apiCredentials, error) {
apiURL := *cfg.endpoint apiURL := *cfg.endpoint
apiURL.Path = path.Join(apiURL.Path, "auth", "tokens") apiURL.Path = path.Join(apiURL.Path, "auth", "tokens")
@ -143,23 +130,19 @@ func getCreds(cfg *apiConfig) (*apiCredentials, error) {
if resp.StatusCode != http.StatusCreated { if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("auth failed, bad status code: %d, want: 201", resp.StatusCode) return nil, fmt.Errorf("auth failed, bad status code: %d, want: 201", resp.StatusCode)
} }
at := resp.Header.Get("X-Subject-Token") at := resp.Header.Get("X-Subject-Token")
if len(at) == 0 { if len(at) == 0 {
return nil, fmt.Errorf("auth failed, response without X-Subject-Token") return nil, fmt.Errorf("auth failed, response without X-Subject-Token")
} }
var ar authResponse var ar authResponse
if err := json.Unmarshal(r, &ar); err != nil { if err := json.Unmarshal(r, &ar); err != nil {
return nil, fmt.Errorf("cannot parse auth credentials response: %w", err) return nil, fmt.Errorf("cannot parse auth credentials response: %w", err)
} }
computeURL, err := getComputeEndpointURL(ar.Token.Catalog, cfg.availability, cfg.region) computeURL, err := getComputeEndpointURL(ar.Token.Catalog, cfg.availability, cfg.region)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get computeEndpoint, account doesn't have enough permissions,"+ return nil, fmt.Errorf("cannot get computeEndpoint, account doesn't have enough permissions, "+
" availability: %s, region: %s", cfg.availability, cfg.region) "availability: %s, region: %s; error: %w", cfg.availability, cfg.region, err)
} }
return &apiCredentials{ return &apiCredentials{
token: at, token: at,
expiration: ar.Token.ExpiresAt, expiration: ar.Token.ExpiresAt,
@ -167,7 +150,7 @@ func getCreds(cfg *apiConfig) (*apiCredentials, error) {
}, nil }, nil
} }
// readResponseBody - reads body from http.Response. // readResponseBody reads body from http.Response.
func readResponseBody(resp *http.Response, apiURL string) ([]byte, error) { func readResponseBody(resp *http.Response, apiURL string) ([]byte, error) {
data, err := ioutil.ReadAll(resp.Body) data, err := ioutil.ReadAll(resp.Body)
_ = resp.Body.Close() _ = resp.Body.Close()
@ -178,26 +161,24 @@ func readResponseBody(resp *http.Response, apiURL string) ([]byte, error) {
return nil, fmt.Errorf("unexpected status code for %q; got %d; want %d; response body: %q", return nil, fmt.Errorf("unexpected status code for %q; got %d; want %d; response body: %q",
apiURL, resp.StatusCode, http.StatusOK, data) apiURL, resp.StatusCode, http.StatusOK, data)
} }
return data, nil return data, nil
} }
// getAPIResponse - makes api call to openstack and returns response body // getAPIResponse calls openstack apiURL and returns response body.
func getAPIResponse(href string, cfg *apiConfig) ([]byte, error) { func getAPIResponse(apiURL string, cfg *apiConfig) ([]byte, error) {
token, err := cfg.getFreshAPICredentials() creds, err := cfg.getFreshAPICredentials()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed refresh api credentials: %w", err) return nil, err
} }
req, err := http.NewRequest("GET", href, nil) req, err := http.NewRequest("GET", apiURL, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create new request for openstack api href: %s, err: %w", href, err) return nil, fmt.Errorf("cannot create new request for openstack api url %s: %w", apiURL, err)
} }
req.Header.Set(authHeaderName, token.token) req.Header.Set("X-Auth-Token", creds.token)
resp, err := cfg.client.Do(req) resp, err := cfg.client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed query openstack api, href: %s, err : %w", href, err) return nil, fmt.Errorf("cannot query openstack api url %s: %w", apiURL, err)
} }
return readResponseBody(resp, apiURL)
return readResponseBody(resp, href)
} }

View file

@ -8,8 +8,9 @@ import (
"time" "time"
) )
// authResponse - identity api response // authResponse represents identity api response
// https://docs.openstack.org/api-ref/identity/v3/?expanded=create-credential-detail,password-authentication-with-unscoped-authorization-detail#authentication-and-token-management //
// See https://docs.openstack.org/api-ref/identity/v3/#authentication-and-token-management
type authResponse struct { type authResponse struct {
Token struct { Token struct {
ExpiresAt time.Time `json:"expires_at,omitempty"` ExpiresAt time.Time `json:"expires_at,omitempty"`
@ -24,7 +25,8 @@ type catalogItem struct {
} }
// openstack api endpoint // openstack api endpoint
// https://docs.openstack.org/api-ref/identity/v3/?expanded=create-credential-detail,password-authentication-with-unscoped-authorization-detail,token-authentication-with-scoped-authorization-detail#list-endpoints //
// See https://docs.openstack.org/api-ref/identity/v3/#list-endpoints
type endpoint struct { type endpoint struct {
RegionID string `json:"region_id"` RegionID string `json:"region_id"`
RegionName string `json:"region_name"` RegionName string `json:"region_name"`
@ -34,33 +36,30 @@ type endpoint struct {
Interface string `json:"interface"` Interface string `json:"interface"`
} }
// getComputeEndpointURL extracts compute url endpoint with given filters from keystone catalog // getComputeEndpointURL extracts compute endpoint url with given filters from keystone catalog
func getComputeEndpointURL(catalog []catalogItem, availability, region string) (*url.URL, error) { func getComputeEndpointURL(catalog []catalogItem, availability, region string) (*url.URL, error) {
for _, eps := range catalog { for _, eps := range catalog {
if eps.Type == "compute" { if eps.Type != "compute" {
for _, ep := range eps.Endpoints { continue
if ep.Interface == availability && (len(region) == 0 || region == ep.RegionID || region == ep.RegionName) { }
return url.Parse(ep.URL) for _, ep := range eps.Endpoints {
} if ep.Interface == availability && (len(region) == 0 || region == ep.RegionID || region == ep.RegionName) {
return url.Parse(ep.URL)
} }
} }
} }
return nil, fmt.Errorf("cannot excract compute url from catalog, availability: %s, region: %s ", availability, region) return nil, fmt.Errorf("cannot find compute url for the given availability: %q, region: %q", availability, region)
} }
// buildAuthRequestBody - builds request for authentication. // buildAuthRequestBody builds request for authentication
func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) { func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
// fast path
if len(sdc.Password) == 0 && len(sdc.ApplicationCredentialID) == 0 && len(sdc.ApplicationCredentialName) == 0 { if len(sdc.Password) == 0 && len(sdc.ApplicationCredentialID) == 0 && len(sdc.ApplicationCredentialName) == 0 {
return nil, fmt.Errorf("password and application credentials is missing") return nil, fmt.Errorf("password and application credentials are missing")
} }
type domainReq struct { type domainReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
} }
type userReq struct { type userReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
@ -68,34 +67,28 @@ func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
Passcode *string `json:"passcode,omitempty"` Passcode *string `json:"passcode,omitempty"`
Domain *domainReq `json:"domain,omitempty"` Domain *domainReq `json:"domain,omitempty"`
} }
type passwordReq struct { type passwordReq struct {
User userReq `json:"user"` User userReq `json:"user"`
} }
type tokenReq struct { type tokenReq struct {
ID string `json:"id"` ID string `json:"id"`
} }
type applicationCredentialReq struct { type applicationCredentialReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
User *userReq `json:"user,omitempty"` User *userReq `json:"user,omitempty"`
Secret *string `json:"secret,omitempty"` Secret *string `json:"secret,omitempty"`
} }
type identityReq struct { type identityReq struct {
Methods []string `json:"methods"` Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"` Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"` Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"` ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
} }
type authReq struct { type authReq struct {
Identity identityReq `json:"identity"` Identity identityReq `json:"identity"`
Scope map[string]interface{} `json:"scope,omitempty"` Scope map[string]interface{} `json:"scope,omitempty"`
} }
type request struct { type request struct {
Auth authReq `json:"auth"` Auth authReq `json:"auth"`
} }
@ -118,84 +111,62 @@ func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
ID: &sdc.ApplicationCredentialID, ID: &sdc.ApplicationCredentialID,
Secret: &sdc.ApplicationCredentialSecret, Secret: &sdc.ApplicationCredentialSecret,
} }
// fast path unscoped
return json.Marshal(req) return json.Marshal(req)
} }
// application_credential_name auth
if len(sdc.ApplicationCredentialSecret) == 0 { if len(sdc.ApplicationCredentialSecret) == 0 {
return nil, fmt.Errorf("application_credential_name is not empty and application_credential_secret is empty") return nil, fmt.Errorf("missing application_credential_secret when application_credential_name is set")
} }
var userRequest *userReq var userRequest *userReq
if len(sdc.UserID) > 0 { if len(sdc.UserID) > 0 {
// UserID could be used without the domain information // UserID could be used without the domain information
userRequest = &userReq{ userRequest = &userReq{
ID: &sdc.UserID, ID: &sdc.UserID,
} }
} }
if userRequest == nil && len(sdc.Username) == 0 { if userRequest == nil && len(sdc.Username) == 0 {
// Make sure that Username or UserID are provided
return nil, fmt.Errorf("username and userid is empty") return nil, fmt.Errorf("username and userid is empty")
} }
if userRequest == nil && len(sdc.DomainID) > 0 { if userRequest == nil && len(sdc.DomainID) > 0 {
userRequest = &userReq{ userRequest = &userReq{
Name: &sdc.Username, Name: &sdc.Username,
Domain: &domainReq{ID: &sdc.DomainID}, Domain: &domainReq{ID: &sdc.DomainID},
} }
} }
if userRequest == nil && len(sdc.DomainName) > 0 { if userRequest == nil && len(sdc.DomainName) > 0 {
userRequest = &userReq{ userRequest = &userReq{
Name: &sdc.Username, Name: &sdc.Username,
Domain: &domainReq{Name: &sdc.DomainName}, Domain: &domainReq{Name: &sdc.DomainName},
} }
} }
// Make sure that domain_id or domain_name are provided among username
if userRequest == nil { if userRequest == nil {
return nil, fmt.Errorf("domain_id and domain_name is empty for application_credential_name auth") return nil, fmt.Errorf("domain_id and domain_name cannot be empty for application_credential_name auth")
} }
req.Auth.Identity.Methods = []string{"application_credential"} req.Auth.Identity.Methods = []string{"application_credential"}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
Name: &sdc.ApplicationCredentialName, Name: &sdc.ApplicationCredentialName,
User: userRequest, User: userRequest,
Secret: &sdc.ApplicationCredentialSecret, Secret: &sdc.ApplicationCredentialSecret,
} }
// fast path unscoped
return json.Marshal(req) return json.Marshal(req)
} }
// Password authentication. // Password authentication.
req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password") req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password")
// At least one of Username and UserID must be specified.
if len(sdc.Username) == 0 && len(sdc.UserID) == 0 { if len(sdc.Username) == 0 && len(sdc.UserID) == 0 {
return nil, fmt.Errorf("username and userid is empty for username/password auth") return nil, fmt.Errorf("username and userid is empty for username/password auth")
} }
if len(sdc.Username) > 0 { if len(sdc.Username) > 0 {
// If Username is provided, UserID may not be provided.
if len(sdc.UserID) > 0 { if len(sdc.UserID) > 0 {
return nil, fmt.Errorf("both username and userid is present") return nil, fmt.Errorf("both username and userid is present")
} }
// Either DomainID or DomainName must also be specified.
if len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 { if len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 {
return nil, fmt.Errorf(" domain_id or domain_name is missing for username/password auth: %s", sdc.Username) return nil, fmt.Errorf(" domain_id or domain_name is missing for username/password auth: %s", sdc.Username)
} }
if len(sdc.DomainID) > 0 { if len(sdc.DomainID) > 0 {
if sdc.DomainName != "" { if sdc.DomainName != "" {
return nil, fmt.Errorf("both domain_id and domain_name is present") return nil, fmt.Errorf("both domain_id and domain_name is present")
} }
// Configure the request for Username and Password authentication with a DomainID. // Configure the request for Username and Password authentication with a DomainID.
if len(sdc.Password) > 0 { if len(sdc.Password) > 0 {
req.Auth.Identity.Password = &passwordReq{ req.Auth.Identity.Password = &passwordReq{
@ -207,7 +178,6 @@ func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
} }
} }
} }
if len(sdc.DomainName) > 0 { if len(sdc.DomainName) > 0 {
// Configure the request for Username and Password authentication with a DomainName. // Configure the request for Username and Password authentication with a DomainName.
if len(sdc.Password) > 0 { if len(sdc.Password) > 0 {
@ -221,16 +191,13 @@ func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
} }
} }
} }
if len(sdc.UserID) > 0 { if len(sdc.UserID) > 0 {
// If UserID is specified, neither DomainID nor DomainName may be.
if len(sdc.DomainID) > 0 { if len(sdc.DomainID) > 0 {
return nil, fmt.Errorf("both user_id and domain_id is present") return nil, fmt.Errorf("both user_id and domain_id is present")
} }
if len(sdc.DomainName) > 0 { if len(sdc.DomainName) > 0 {
return nil, fmt.Errorf("both user_id and domain_name is present") return nil, fmt.Errorf("both user_id and domain_name is present")
} }
// Configure the request for UserID and Password authentication. // Configure the request for UserID and Password authentication.
if len(sdc.Password) > 0 { if len(sdc.Password) > 0 {
req.Auth.Identity.Password = &passwordReq{ req.Auth.Identity.Password = &passwordReq{
@ -251,19 +218,16 @@ func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
if len(scope) > 0 { if len(scope) > 0 {
req.Auth.Scope = scope req.Auth.Scope = scope
} }
return json.Marshal(req) return json.Marshal(req)
} }
// buildScope - adds scope information into auth request // buildScope adds scope information into auth request
// https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-scoped-authorization-detail#password-authentication-with-unscoped-authorization //
// See https://docs.openstack.org/api-ref/identity/v3/#password-authentication-with-unscoped-authorization
func buildScope(sdc *SDConfig) (map[string]interface{}, error) { func buildScope(sdc *SDConfig) (map[string]interface{}, error) {
// fast path
if len(sdc.ProjectName) == 0 && len(sdc.ProjectID) == 0 && len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 { if len(sdc.ProjectName) == 0 && len(sdc.ProjectID) == 0 && len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 {
return nil, nil return nil, nil
} }
if len(sdc.ProjectName) > 0 { if len(sdc.ProjectName) > 0 {
// ProjectName provided: either DomainID or DomainName must also be supplied. // ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied. // ProjectID may not be supplied.
@ -273,10 +237,7 @@ func buildScope(sdc *SDConfig) (map[string]interface{}, error) {
if len(sdc.ProjectID) > 0 { if len(sdc.ProjectID) > 0 {
return nil, fmt.Errorf("both domain_id and domain_name present") return nil, fmt.Errorf("both domain_id and domain_name present")
} }
if len(sdc.DomainID) > 0 { if len(sdc.DomainID) > 0 {
// ProjectName + DomainID
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"name": &sdc.ProjectName, "name": &sdc.ProjectName,
@ -284,10 +245,7 @@ func buildScope(sdc *SDConfig) (map[string]interface{}, error) {
}, },
}, nil }, nil
} }
if len(sdc.DomainName) > 0 { if len(sdc.DomainName) > 0 {
// ProjectName + DomainName
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"name": &sdc.ProjectName, "name": &sdc.ProjectName,
@ -303,39 +261,31 @@ func buildScope(sdc *SDConfig) (map[string]interface{}, error) {
if len(sdc.DomainName) > 0 { if len(sdc.DomainName) > 0 {
return nil, fmt.Errorf("both project_id and domain_name present") return nil, fmt.Errorf("both project_id and domain_name present")
} }
// ProjectID
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"id": &sdc.ProjectID, "id": &sdc.ProjectID,
}, },
}, nil }, nil
} else if len(sdc.DomainID) > 0 { } else if len(sdc.DomainID) > 0 {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if len(sdc.DomainName) > 0 { if len(sdc.DomainName) > 0 {
return nil, fmt.Errorf("both domain_id and domain_name present") return nil, fmt.Errorf("both domain_id and domain_name present")
} }
// DomainID
return map[string]interface{}{ return map[string]interface{}{
"domain": map[string]interface{}{ "domain": map[string]interface{}{
"id": &sdc.DomainID, "id": &sdc.DomainID,
}, },
}, nil }, nil
} else if len(sdc.DomainName) > 0 { } else if len(sdc.DomainName) > 0 {
// DomainName
return map[string]interface{}{ return map[string]interface{}{
"domain": map[string]interface{}{ "domain": map[string]interface{}{
"name": &sdc.DomainName, "name": &sdc.DomainName,
}, },
}, nil }, nil
} }
return nil, nil return nil, nil
} }
// readCredentialsFromEnv - obtains serviceDiscoveryConfig from env variables for openstack // readCredentialsFromEnv obtains serviceDiscoveryConfig from env variables for openstack
func readCredentialsFromEnv() SDConfig { func readCredentialsFromEnv() SDConfig {
authURL := os.Getenv("OS_AUTH_URL") authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME") username := os.Getenv("OS_USERNAME")
@ -352,7 +302,6 @@ func readCredentialsFromEnv() SDConfig {
if v := os.Getenv("OS_PROJECT_ID"); v != "" { if v := os.Getenv("OS_PROJECT_ID"); v != "" {
tenantID = v tenantID = v
} }
// If OS_PROJECT_NAME is set, overwrite tenantName with the value. // If OS_PROJECT_NAME is set, overwrite tenantName with the value.
if v := os.Getenv("OS_PROJECT_NAME"); v != "" { if v := os.Getenv("OS_PROJECT_NAME"); v != "" {
tenantName = v tenantName = v

View file

@ -9,7 +9,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// https://docs.openstack.org/api-ref/compute/?expanded=list-servers-detailed-detail#list-hypervisors-details // See https://docs.openstack.org/api-ref/compute/#list-hypervisors-details
type hypervisorDetail struct { type hypervisorDetail struct {
Hypervisors []hypervisor `json:"hypervisors"` Hypervisors []hypervisor `json:"hypervisors"`
Links []struct { Links []struct {
@ -32,12 +32,15 @@ func parseHypervisorDetail(data []byte) (*hypervisorDetail, error) {
if err := json.Unmarshal(data, &hvsd); err != nil { if err := json.Unmarshal(data, &hvsd); err != nil {
return nil, fmt.Errorf("cannot parse hypervisorDetail: %w", err) return nil, fmt.Errorf("cannot parse hypervisorDetail: %w", err)
} }
return &hvsd, nil return &hvsd, nil
} }
func (cfg *apiConfig) getHypervisors() ([]hypervisor, error) { func (cfg *apiConfig) getHypervisors() ([]hypervisor, error) {
computeURL := *cfg.creds.computeURL creds, err := cfg.getFreshAPICredentials()
if err != nil {
return nil, err
}
computeURL := *creds.computeURL
computeURL.Path = path.Join(computeURL.Path, "os-hypervisors", "detail") computeURL.Path = path.Join(computeURL.Path, "os-hypervisors", "detail")
nextLink := computeURL.String() nextLink := computeURL.String()
var hvs []hypervisor var hvs []hypervisor
@ -46,19 +49,15 @@ func (cfg *apiConfig) getHypervisors() ([]hypervisor, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
detail, err := parseHypervisorDetail(resp) detail, err := parseHypervisorDetail(resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hvs = append(hvs, detail.Hypervisors...) hvs = append(hvs, detail.Hypervisors...)
if len(detail.Links) == 0 {
if len(detail.Links) > 0 { return hvs, nil
nextLink = detail.Links[0].HREF
continue
} }
nextLink = detail.Links[0].HREF
return hvs, nil
} }
} }
@ -77,7 +76,6 @@ func addHypervisorLabels(hvs []hypervisor, port int) []map[string]string {
} }
ms = append(ms, m) ms = append(ms, m)
} }
return ms return ms
} }
@ -86,6 +84,5 @@ func getHypervisorLabels(cfg *apiConfig) ([]map[string]string, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get hypervisors: %w", err) return nil, fmt.Errorf("cannot get hypervisors: %w", err)
} }
return addHypervisorLabels(hvs, cfg.port), nil return addHypervisorLabels(hvs, cfg.port), nil
} }

View file

@ -4,11 +4,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"path" "path"
"sort"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
) )
// https://docs.openstack.org/api-ref/compute/?expanded=list-servers-detailed-detail#list-servers // See https://docs.openstack.org/api-ref/compute/#list-servers
type serversDetail struct { type serversDetail struct {
Servers []server `json:"servers"` Servers []server `json:"servers"`
Links []struct { Links []struct {
@ -40,7 +41,6 @@ func parseServersDetail(data []byte) (*serversDetail, error) {
if err := json.Unmarshal(data, &srvd); err != nil { if err := json.Unmarshal(data, &srvd); err != nil {
return nil, fmt.Errorf("cannot parse serversDetail: %w", err) return nil, fmt.Errorf("cannot parse serversDetail: %w", err)
} }
return &srvd, nil return &srvd, nil
} }
@ -55,13 +55,20 @@ func addInstanceLabels(servers []server, port int) []map[string]string {
"__meta_openstack_user_id": server.UserID, "__meta_openstack_user_id": server.UserID,
"__meta_openstack_instance_flavor": server.Flavor.ID, "__meta_openstack_instance_flavor": server.Flavor.ID,
} }
for k, v := range server.Metadata { for k, v := range server.Metadata {
m["__meta_openstack_tag_"+discoveryutils.SanitizeLabelName(k)] = v m["__meta_openstack_tag_"+discoveryutils.SanitizeLabelName(k)] = v
} }
for pool, addresses := range server.Addresses { // Traverse server.Addresses in alphabetical order of pool name
// in order to return targets in deterministic order.
sortedPools := make([]string, 0, len(server.Addresses))
for pool := range server.Addresses {
sortedPools = append(sortedPools, pool)
}
sort.Strings(sortedPools)
for _, pool := range sortedPools {
addresses := server.Addresses[pool]
if len(addresses) == 0 { if len(addresses) == 0 {
// pool with zero addresses skip it // skip pool with zero addresses
continue continue
} }
var publicIP string var publicIP string
@ -90,7 +97,6 @@ func addInstanceLabels(servers []server, port int) []map[string]string {
} }
lbls["__address__"] = discoveryutils.JoinHostPort(ip.Address, port) lbls["__address__"] = discoveryutils.JoinHostPort(ip.Address, port)
ms = append(ms, lbls) ms = append(ms, lbls)
} }
} }
} }
@ -98,7 +104,11 @@ func addInstanceLabels(servers []server, port int) []map[string]string {
} }
func (cfg *apiConfig) getServers() ([]server, error) { func (cfg *apiConfig) getServers() ([]server, error) {
computeURL := *cfg.creds.computeURL creds, err := cfg.getFreshAPICredentials()
if err != nil {
return nil, err
}
computeURL := *creds.computeURL
computeURL.Path = path.Join(computeURL.Path, "servers", "detail") computeURL.Path = path.Join(computeURL.Path, "servers", "detail")
// by default, query fetches data from all tenants // by default, query fetches data from all tenants
if !cfg.allTenants { if !cfg.allTenants {
@ -106,28 +116,22 @@ func (cfg *apiConfig) getServers() ([]server, error) {
q.Set("all_tenants", "false") q.Set("all_tenants", "false")
computeURL.RawQuery = q.Encode() computeURL.RawQuery = q.Encode()
} }
nextLink := computeURL.String() nextLink := computeURL.String()
var servers []server var servers []server
for { for {
resp, err := getAPIResponse(nextLink, cfg) resp, err := getAPIResponse(nextLink, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
serversDetail, err := parseServersDetail(resp) serversDetail, err := parseServersDetail(resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
servers = append(servers, serversDetail.Servers...) servers = append(servers, serversDetail.Servers...)
if len(serversDetail.Links) == 0 {
if len(serversDetail.Links) > 0 { return servers, nil
nextLink = serversDetail.Links[0].HREF
continue
} }
nextLink = serversDetail.Links[0].HREF
return servers, nil
} }
} }
@ -137,5 +141,4 @@ func getInstancesLabels(cfg *apiConfig) ([]map[string]string, error) {
return nil, err return nil, err
} }
return addInstanceLabels(srv, cfg.port), nil return addInstanceLabels(srv, cfg.port), nil
} }

View file

@ -19,13 +19,13 @@ func Test_addInstanceLabels(t *testing.T) {
want [][]prompbmarshal.Label want [][]prompbmarshal.Label
}{ }{
{ {
name: "empty response", name: "empty_response",
args: args{ args: args{
port: 9100, port: 9100,
}, },
}, },
{ {
name: "1 server", name: "one_server",
args: args{ args: args{
port: 9100, port: 9100,
servers: []server{ servers: []server{
@ -70,7 +70,7 @@ func Test_addInstanceLabels(t *testing.T) {
}, },
}, },
{ {
name: "with public ip", name: "with_public_ip",
args: args{ args: args{
port: 9100, port: 9100,
servers: []server{ servers: []server{
@ -113,6 +113,17 @@ func Test_addInstanceLabels(t *testing.T) {
}, },
}, },
want: [][]prompbmarshal.Label{ want: [][]prompbmarshal.Label{
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.10.0.1:9100",
"__meta_openstack_address_pool": "internal",
"__meta_openstack_instance_flavor": "5",
"__meta_openstack_instance_id": "10",
"__meta_openstack_instance_name": "server-2",
"__meta_openstack_instance_status": "enabled",
"__meta_openstack_private_ip": "10.10.0.1",
"__meta_openstack_project_id": "some-tenant-id",
"__meta_openstack_user_id": "some-user-id",
}),
discoveryutils.GetSortedLabels(map[string]string{ discoveryutils.GetSortedLabels(map[string]string{
"__address__": "192.168.0.1:9100", "__address__": "192.168.0.1:9100",
"__meta_openstack_address_pool": "test", "__meta_openstack_address_pool": "test",
@ -125,24 +136,12 @@ func Test_addInstanceLabels(t *testing.T) {
"__meta_openstack_project_id": "some-tenant-id", "__meta_openstack_project_id": "some-tenant-id",
"__meta_openstack_user_id": "some-user-id", "__meta_openstack_user_id": "some-user-id",
}), }),
discoveryutils.GetSortedLabels(map[string]string{
"__address__": "10.10.0.1:9100",
"__meta_openstack_address_pool": "internal",
"__meta_openstack_instance_flavor": "5",
"__meta_openstack_instance_id": "10",
"__meta_openstack_instance_name": "server-2",
"__meta_openstack_instance_status": "enabled",
"__meta_openstack_private_ip": "10.10.0.1",
"__meta_openstack_project_id": "some-tenant-id",
"__meta_openstack_user_id": "some-user-id",
}),
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := addInstanceLabels(tt.args.servers, tt.args.port) got := addInstanceLabels(tt.args.servers, tt.args.port)
var sortedLabelss [][]prompbmarshal.Label var sortedLabelss [][]prompbmarshal.Label
for _, labels := range got { for _, labels := range got {
sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels)) sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))

View file

@ -7,24 +7,28 @@ import (
) )
// SDConfig is the configuration for OpenStack based service discovery. // SDConfig is the configuration for OpenStack based service discovery.
//
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config
type SDConfig struct { type SDConfig struct {
IdentityEndpoint string `yaml:"identity_endpoint"` IdentityEndpoint string `yaml:"identity_endpoint"`
Username string `yaml:"username"` Username string `yaml:"username"`
UserID string `yaml:"userid"` UserID string `yaml:"userid"`
Password string `yaml:"password"` Password string `yaml:"password"`
ProjectName string `yaml:"project_name"` ProjectName string `yaml:"project_name"`
ProjectID string `yaml:"project_id"` ProjectID string `yaml:"project_id"`
DomainName string `yaml:"domain_name"` DomainName string `yaml:"domain_name"`
DomainID string `yaml:"domain_id"` DomainID string `yaml:"domain_id"`
ApplicationCredentialName string `yaml:"application_credential_name"` ApplicationCredentialName string `yaml:"application_credential_name"`
ApplicationCredentialID string `yaml:"application_credential_id"` ApplicationCredentialID string `yaml:"application_credential_id"`
ApplicationCredentialSecret string `yaml:"application_credential_secret"` ApplicationCredentialSecret string `yaml:"application_credential_secret"`
Role string `yaml:"role"` Role string `yaml:"role"`
Region string `yaml:"region"` Region string `yaml:"region"`
Port int `yaml:"port"` // RefreshInterval time.Duration `yaml:"refresh_interval"`
AllTenants bool `yaml:"all_tenants"` // refresh_interval is obtained from `-promscrape.openstackSDCheckInterval` command-line option.
TLSConfig *promauth.TLSConfig `yaml:"tls_config"` Port int `yaml:"port"`
Availability string `yaml:"availability"` AllTenants bool `yaml:"all_tenants"`
TLSConfig *promauth.TLSConfig `yaml:"tls_config"`
Availability string `yaml:"availability"`
} }
// GetLabels returns gce labels according to sdc. // GetLabels returns gce labels according to sdc.