Add Uyuni service discovery (#8190)

* Add Uyuni service discovery

Signed-off-by: Witek Bedyk <witold.bedyk@suse.com>

Co-authored-by: Joao Cavalheiro <jcavalheiro@suse.de>
Co-authored-by: Marcelo Chiaradia <mchiaradia@suse.com>
Co-authored-by: Stefano Torresi <stefano@torresi.io>
Co-authored-by: Julien Pivotto <roidelapluie@gmail.com>
This commit is contained in:
Witek Bedyk 2021-10-19 01:00:44 +02:00 committed by GitHub
parent a4ad290987
commit cda2dbbef6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 490 additions and 1 deletions

View file

@ -49,6 +49,7 @@ import (
"github.com/prometheus/prometheus/discovery/scaleway" "github.com/prometheus/prometheus/discovery/scaleway"
"github.com/prometheus/prometheus/discovery/targetgroup" "github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/discovery/triton" "github.com/prometheus/prometheus/discovery/triton"
"github.com/prometheus/prometheus/discovery/uyuni"
"github.com/prometheus/prometheus/discovery/xds" "github.com/prometheus/prometheus/discovery/xds"
"github.com/prometheus/prometheus/discovery/zookeeper" "github.com/prometheus/prometheus/discovery/zookeeper"
"github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/labels"
@ -934,6 +935,26 @@ var expectedConf = &Config{
}, },
}, },
}, },
{
JobName: "uyuni",
HonorTimestamps: true,
ScrapeInterval: model.Duration(15 * time.Second),
ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
HTTPClientConfig: config.HTTPClientConfig{FollowRedirects: true},
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
ServiceDiscoveryConfigs: discovery.Configs{
&uyuni.SDConfig{
Server: kubernetesSDHostURL(),
Username: "gopher",
Password: "hole",
Entitlement: "monitoring_entitled",
Separator: ",",
RefreshInterval: model.Duration(60 * time.Second),
},
},
},
}, },
AlertingConfig: AlertingConfig{ AlertingConfig: AlertingConfig{
AlertmanagerConfigs: []*AlertmanagerConfig{ AlertmanagerConfigs: []*AlertmanagerConfig{
@ -1018,7 +1039,7 @@ func TestElideSecrets(t *testing.T) {
yamlConfig := string(config) yamlConfig := string(config)
matches := secretRe.FindAllStringIndex(yamlConfig, -1) matches := secretRe.FindAllStringIndex(yamlConfig, -1)
require.Equal(t, 15, len(matches), "wrong number of secret matches found") require.Equal(t, 16, len(matches), "wrong number of secret matches found")
require.NotContains(t, yamlConfig, "mysecret", require.NotContains(t, yamlConfig, "mysecret",
"yaml marshal reveals authentication credentials.") "yaml marshal reveals authentication credentials.")
} }

View file

@ -349,6 +349,12 @@ scrape_configs:
- authorization: - authorization:
credentials: abcdef credentials: abcdef
- job_name: uyuni
uyuni_sd_configs:
- server: https://localhost:1234
username: gopher
password: hole
alerting: alerting:
alertmanagers: alertmanagers:
- scheme: https - scheme: https

View file

@ -34,6 +34,7 @@ import (
_ "github.com/prometheus/prometheus/discovery/puppetdb" // register puppetdb _ "github.com/prometheus/prometheus/discovery/puppetdb" // register puppetdb
_ "github.com/prometheus/prometheus/discovery/scaleway" // register scaleway _ "github.com/prometheus/prometheus/discovery/scaleway" // register scaleway
_ "github.com/prometheus/prometheus/discovery/triton" // register triton _ "github.com/prometheus/prometheus/discovery/triton" // register triton
_ "github.com/prometheus/prometheus/discovery/uyuni" // register uyuni
_ "github.com/prometheus/prometheus/discovery/xds" // register xds _ "github.com/prometheus/prometheus/discovery/xds" // register xds
_ "github.com/prometheus/prometheus/discovery/zookeeper" // register zookeeper _ "github.com/prometheus/prometheus/discovery/zookeeper" // register zookeeper
) )

341
discovery/uyuni/uyuni.go Normal file
View file

@ -0,0 +1,341 @@
// Copyright 2020 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package uyuni
import (
"context"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/kolo/xmlrpc"
"github.com/pkg/errors"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
const (
uyuniXMLRPCAPIPath = "/rpc/api"
uyuniMetaLabelPrefix = model.MetaLabelPrefix + "uyuni_"
uyuniLabelMinionHostname = uyuniMetaLabelPrefix + "minion_hostname"
uyuniLabelPrimaryFQDN = uyuniMetaLabelPrefix + "primary_fqdn"
uyuniLablelSystemID = uyuniMetaLabelPrefix + "system_id"
uyuniLablelGroups = uyuniMetaLabelPrefix + "groups"
uyuniLablelEndpointName = uyuniMetaLabelPrefix + "endpoint_name"
uyuniLablelExporter = uyuniMetaLabelPrefix + "exporter"
uyuniLabelProxyModule = uyuniMetaLabelPrefix + "proxy_module"
uyuniLabelMetricsPath = uyuniMetaLabelPrefix + "metrics_path"
uyuniLabelScheme = uyuniMetaLabelPrefix + "scheme"
)
// DefaultSDConfig is the default Uyuni SD configuration.
var DefaultSDConfig = SDConfig{
Entitlement: "monitoring_entitled",
Separator: ",",
RefreshInterval: model.Duration(1 * time.Minute),
}
func init() {
discovery.RegisterConfig(&SDConfig{})
}
// SDConfig is the configuration for Uyuni based service discovery.
type SDConfig struct {
Server config.URL `yaml:"server"`
Username string `yaml:"username"`
Password config.Secret `yaml:"password"`
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
Entitlement string `yaml:"entitlement,omitempty"`
Separator string `yaml:"separator,omitempty"`
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
}
// Uyuni API Response structures
type systemGroupID struct {
GroupID int `xmlrpc:"id"`
GroupName string `xmlrpc:"name"`
}
type networkInfo struct {
SystemID int `xmlrpc:"system_id"`
Hostname string `xmlrpc:"hostname"`
PrimaryFQDN string `xmlrpc:"primary_fqdn"`
IP string `xmlrpc:"ip"`
}
type endpointInfo struct {
SystemID int `xmlrpc:"system_id"`
EndpointName string `xmlrpc:"endpoint_name"`
Port int `xmlrpc:"port"`
Path string `xmlrpc:"path"`
Module string `xmlrpc:"module"`
ExporterName string `xmlrpc:"exporter_name"`
TLSEnabled bool `xmlrpc:"tls_enabled"`
}
// Discovery periodically performs Uyuni API requests. It implements the Discoverer interface.
type Discovery struct {
*refresh.Discovery
apiURL *url.URL
roundTripper http.RoundTripper
username string
password string
entitlement string
separator string
interval time.Duration
logger log.Logger
}
// Name returns the name of the Config.
func (*SDConfig) Name() string { return "uyuni" }
// NewDiscoverer returns a Discoverer for the Config.
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
return NewDiscovery(c, opts.Logger)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultSDConfig
type plain SDConfig
err := unmarshal((*plain)(c))
if err != nil {
return err
}
if c.Server.URL == nil {
return errors.New("Uyuni SD configuration requires server host")
}
_, err = url.Parse(c.Server.String())
if err != nil {
return errors.Wrap(err, "Uyuni Server URL is not valid")
}
if c.Username == "" {
return errors.New("Uyuni SD configuration requires a username")
}
if c.Password == "" {
return errors.New("Uyuni SD configuration requires a password")
}
return nil
}
// Attempt to login in Uyuni Server and get an auth token
func login(rpcclient *xmlrpc.Client, user string, pass string) (string, error) {
var result string
err := rpcclient.Call("auth.login", []interface{}{user, pass}, &result)
return result, err
}
// Logout from Uyuni API
func logout(rpcclient *xmlrpc.Client, token string) error {
return rpcclient.Call("auth.logout", token, nil)
}
// Get the system groups information of monitored clients
func getSystemGroupsInfoOfMonitoredClients(rpcclient *xmlrpc.Client, token string, entitlement string) (map[int][]systemGroupID, error) {
var systemGroupsInfos []struct {
SystemID int `xmlrpc:"id"`
SystemGroups []systemGroupID `xmlrpc:"system_groups"`
}
err := rpcclient.Call("system.listSystemGroupsForSystemsWithEntitlement", []interface{}{token, entitlement}, &systemGroupsInfos)
if err != nil {
return nil, err
}
result := make(map[int][]systemGroupID)
for _, systemGroupsInfo := range systemGroupsInfos {
result[systemGroupsInfo.SystemID] = systemGroupsInfo.SystemGroups
}
return result, nil
}
// GetSystemNetworkInfo lists client FQDNs.
func getNetworkInformationForSystems(rpcclient *xmlrpc.Client, token string, systemIDs []int) (map[int]networkInfo, error) {
var networkInfos []networkInfo
err := rpcclient.Call("system.getNetworkForSystems", []interface{}{token, systemIDs}, &networkInfos)
if err != nil {
return nil, err
}
result := make(map[int]networkInfo)
for _, networkInfo := range networkInfos {
result[networkInfo.SystemID] = networkInfo
}
return result, nil
}
// Get endpoints information for given systems
func getEndpointInfoForSystems(
rpcclient *xmlrpc.Client,
token string,
systemIDs []int,
) ([]endpointInfo, error) {
var endpointInfos []endpointInfo
err := rpcclient.Call(
"system.monitoring.listEndpoints",
[]interface{}{token, systemIDs}, &endpointInfos)
if err != nil {
return nil, err
}
return endpointInfos, err
}
// NewDiscovery returns a uyuni discovery for the given configuration.
func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) {
var apiURL *url.URL
*apiURL = *conf.Server.URL
apiURL.Path = path.Join(apiURL.Path, uyuniXMLRPCAPIPath)
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "uyuni_sd", config.WithHTTP2Disabled())
if err != nil {
return nil, err
}
d := &Discovery{
apiURL: apiURL,
roundTripper: rt,
username: conf.Username,
password: string(conf.Password),
entitlement: conf.Entitlement,
separator: conf.Separator,
interval: time.Duration(conf.RefreshInterval),
logger: logger,
}
d.Discovery = refresh.NewDiscovery(
logger,
"uyuni",
time.Duration(conf.RefreshInterval),
d.refresh,
)
return d, nil
}
func (d *Discovery) getEndpointLabels(
endpoint endpointInfo,
systemGroupIDs []systemGroupID,
networkInfo networkInfo,
) model.LabelSet {
var addr, scheme string
managedGroupNames := getSystemGroupNames(systemGroupIDs)
addr = fmt.Sprintf("%s:%d", networkInfo.Hostname, endpoint.Port)
if endpoint.TLSEnabled {
scheme = "https"
} else {
scheme = "http"
}
result := model.LabelSet{
model.AddressLabel: model.LabelValue(addr),
uyuniLabelMinionHostname: model.LabelValue(networkInfo.Hostname),
uyuniLabelPrimaryFQDN: model.LabelValue(networkInfo.PrimaryFQDN),
uyuniLablelSystemID: model.LabelValue(fmt.Sprintf("%d", endpoint.SystemID)),
uyuniLablelGroups: model.LabelValue(strings.Join(managedGroupNames, d.separator)),
uyuniLablelEndpointName: model.LabelValue(endpoint.EndpointName),
uyuniLablelExporter: model.LabelValue(endpoint.ExporterName),
uyuniLabelProxyModule: model.LabelValue(endpoint.Module),
uyuniLabelMetricsPath: model.LabelValue(endpoint.Path),
uyuniLabelScheme: model.LabelValue(scheme),
}
return result
}
func getSystemGroupNames(systemGroupsIDs []systemGroupID) []string {
managedGroupNames := make([]string, 0, len(systemGroupsIDs))
for _, systemGroupInfo := range systemGroupsIDs {
managedGroupNames = append(managedGroupNames, systemGroupInfo.GroupName)
}
return managedGroupNames
}
func (d *Discovery) getTargetsForSystems(
rpcClient *xmlrpc.Client,
token string,
entitlement string,
) ([]model.LabelSet, error) {
result := make([]model.LabelSet, 0)
systemGroupIDsBySystemID, err := getSystemGroupsInfoOfMonitoredClients(rpcClient, token, entitlement)
if err != nil {
return nil, errors.Wrap(err, "unable to get the managed system groups information of monitored clients")
}
systemIDs := make([]int, 0, len(systemGroupIDsBySystemID))
for systemID := range systemGroupIDsBySystemID {
systemIDs = append(systemIDs, systemID)
}
endpointInfos, err := getEndpointInfoForSystems(rpcClient, token, systemIDs)
if err != nil {
return nil, errors.Wrap(err, "unable to get endpoints information")
}
networkInfoBySystemID, err := getNetworkInformationForSystems(rpcClient, token, systemIDs)
if err != nil {
return nil, errors.Wrap(err, "unable to get the systems network information")
}
for _, endpoint := range endpointInfos {
systemID := endpoint.SystemID
labels := d.getEndpointLabels(
endpoint,
systemGroupIDsBySystemID[systemID],
networkInfoBySystemID[systemID])
result = append(result, labels)
}
return result, nil
}
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
rpcClient, err := xmlrpc.NewClient(d.apiURL.String(), d.roundTripper)
if err != nil {
return nil, err
}
defer rpcClient.Close()
token, err := login(rpcClient, d.username, d.password)
if err != nil {
return nil, errors.Wrap(err, "unable to login to Uyuni API")
}
defer func() {
if err := logout(rpcClient, token); err != nil {
level.Debug(d.logger).Log("msg", "Failed to log out from Uyuni API", "err", err)
}
}()
targetsForSystems, err := d.getTargetsForSystems(rpcClient, token, d.entitlement)
if err != nil {
return nil, err
}
return []*targetgroup.Group{{Targets: targetsForSystems, Source: d.apiURL.String()}}, nil
}

View file

@ -288,6 +288,10 @@ serverset_sd_configs:
triton_sd_configs: triton_sd_configs:
[ - <triton_sd_config> ... ] [ - <triton_sd_config> ... ]
# List of Uyuni service discovery configurations.
uyuni_sd_configs:
[ - <uyuni_sd_config> ... ]
# List of labeled statically configured targets for this job. # List of labeled statically configured targets for this job.
static_configs: static_configs:
[ - <static_config> ... ] [ - <static_config> ... ]
@ -2256,6 +2260,79 @@ tls_config:
[ <tls_config> ] [ <tls_config> ]
``` ```
### `<uyuni_sd_config>`
Uyuni SD configurations allow retrieving scrape targets from managed systems
via [Uyuni](https://www.uyuni-project.org/) API.
The following meta labels are available on targets during [relabeling](#relabel_config):
* `__meta_uyuni_endpoint_name`: the name of the application endpoint
* `__meta_uyuni_exporter`: the exporter exposing metrics for the target
* `__meta_uyuni_groups`: the system groups of the target
* `__meta_uyuni_metrics_path`: metrics path for the target
* `__meta_uyuni_minion_hostname`: hostname of the Uyuni client
* `__meta_uyuni_primary_fqdn`: primary FQDN of the Uyuni client
* `__meta_uyuni_proxy_module`: the module name if _Exporter Exporter_ proxy is
configured for the target
* `__meta_uyuni_scheme`: the protocol scheme used for requests
* `__meta_uyuni_system_id`: the system ID of the client
See below for the configuration options for Uyuni discovery:
```yaml
# The URL to connect to the Uyuni server.
server: <string>
# Credentials are used to authenticate the requests to Uyuni API.
username: <string>
password: <secret>
# The entitlement string to filter eligible systems.
[ entitlement: <string> | default = monitoring_entitled ]
# The string by which Uyuni group names are joined into the groups label.
[ separator: <string> | default = , ]
# Refresh interval to re-read the managed targets list.
[ refresh_interval: <duration> | default = 60s ]
# Optional HTTP basic authentication information, currently not supported by Uyuni.
basic_auth:
[ username: <string> ]
[ password: <secret> ]
[ password_file: <string> ]
# Optional `Authorization` header configuration, currently not supported by Uyuni.
authorization:
# Sets the authentication type.
[ type: <string> | default: Bearer ]
# Sets the credentials. It is mutually exclusive with
# `credentials_file`.
[ credentials: <secret> ]
# Sets the credentials to the credentials read from the configured file.
# It is mutually exclusive with `credentials`.
[ credentials_file: <filename> ]
# Optional OAuth 2.0 configuration, currently not supported by Uyuni.
# Cannot be used at the same time as basic_auth or authorization.
oauth2:
[ <oauth2> ]
# Optional proxy URL.
[ proxy_url: <string> ]
# Configure whether HTTP requests follow HTTP 3xx redirects.
[ follow_redirects: <bool> | default = true ]
# TLS configuration.
tls_config:
[ <tls_config> ]
```
See [the Prometheus uyuni-sd configuration file](/documentation/examples/prometheus-uyuni.yml)
for a practical example on how to set up Uyuni Prometheus configuration.
### `<static_config>` ### `<static_config>`
A `static_config` allows specifying a list of targets and a common label set A `static_config` allows specifying a list of targets and a common label set
@ -2518,6 +2595,10 @@ serverset_sd_configs:
triton_sd_configs: triton_sd_configs:
[ - <triton_sd_config> ... ] [ - <triton_sd_config> ... ]
# List of Uyuni service discovery configurations.
uyuni_sd_configs:
[ - <uyuni_sd_config> ... ]
# List of labeled statically configured Alertmanagers. # List of labeled statically configured Alertmanagers.
static_configs: static_configs:
[ - <static_config> ... ] [ - <static_config> ... ]

View file

@ -0,0 +1,36 @@
# A example scrape configuration for running Prometheus with Uyuni.
scrape_configs:
# Make Prometheus scrape itself for metrics.
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Discover Uyuni managed targets to scrape.
- job_name: 'uyuni'
# Scrape Uyuni itself to discover new services.
uyuni_sd_configs:
- server: http://uyuni-project.org
username: gopher
password: hole
relabel_configs:
- source_labels: [__meta_uyuni_exporter]
target_label: exporter
- source_labels: [__meta_uyuni_groups]
target_label: groups
- source_labels: [__meta_uyuni_minion_hostname]
target_label: hostname
- source_labels: [__meta_uyuni_primary_fqdn]
regex: (.+)
target_label: hostname
- source_labels: [hostname, __address__]
regex: (.*);.*:(.*)
replacement: ${1}:${2}
target_label: __address__
- source_labels: [__meta_uyuni_metrics_path]
regex: (.+)
target_label: __metrics_path__
- source_labels: [__meta_uyuni_proxy_module]
target_label: __param_module

1
go.mod
View file

@ -33,6 +33,7 @@ require (
github.com/hetznercloud/hcloud-go v1.32.0 github.com/hetznercloud/hcloud-go v1.32.0
github.com/influxdata/influxdb v1.9.3 github.com/influxdata/influxdb v1.9.3
github.com/json-iterator/go v1.1.11 github.com/json-iterator/go v1.1.11
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b
github.com/linode/linodego v0.32.0 github.com/linode/linodego v0.32.0
github.com/miekg/dns v1.1.43 github.com/miekg/dns v1.1.43
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect

2
go.sum
View file

@ -886,6 +886,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b h1:iNjcivnc6lhbvJA3LD622NPrUponluJrBWPIwGG/3Bg=
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=