Browse Source

Add service discovery for OvhCloud (#10802)

* feat(ovhcloud): add ovhcloud management

Signed-off-by: Marine Bal <marine.bal@corp.ovh.com>
Co-authored-by: Arnaud Sinays <sinaysarnaud@gmail.com>
pull/11529/head
Marine Bal 2 years ago committed by GitHub
parent
commit
16c3aa75c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      config/config_test.go
  2. 15
      config/testdata/conf.good.yml
  3. 8
      config/testdata/ovhcloud_bad_service.bad.yml
  4. 7
      config/testdata/ovhcloud_no_secret.bad.yml
  5. 1
      discovery/install/install.go
  6. 161
      discovery/ovhcloud/dedicated_server.go
  7. 123
      discovery/ovhcloud/dedicated_server_test.go
  8. 155
      discovery/ovhcloud/ovhcloud.go
  9. 129
      discovery/ovhcloud/ovhcloud_test.go
  10. 3
      discovery/ovhcloud/testdata/dedicated_server/dedicated_servers.json
  11. 4
      discovery/ovhcloud/testdata/dedicated_server/dedicated_servers_abcde_ips.json
  12. 20
      discovery/ovhcloud/testdata/dedicated_server/dedicated_servers_details.json
  13. 3
      discovery/ovhcloud/testdata/vps/vps.json
  14. 4
      discovery/ovhcloud/testdata/vps/vps_abc_ips.json
  15. 25
      discovery/ovhcloud/testdata/vps/vps_details.json
  16. 186
      discovery/ovhcloud/vps.go
  17. 130
      discovery/ovhcloud/vps_test.go
  18. 70
      docs/configuration/configuration.md
  19. 16
      documentation/examples/prometheus-ovhcloud.yml
  20. 1
      go.mod
  21. 3
      go.sum
  22. 1
      plugins.yml
  23. 3
      plugins/plugins.go

40
config/config_test.go

@ -47,6 +47,7 @@ import (
"github.com/prometheus/prometheus/discovery/moby"
"github.com/prometheus/prometheus/discovery/nomad"
"github.com/prometheus/prometheus/discovery/openstack"
"github.com/prometheus/prometheus/discovery/ovhcloud"
"github.com/prometheus/prometheus/discovery/puppetdb"
"github.com/prometheus/prometheus/discovery/scaleway"
"github.com/prometheus/prometheus/discovery/targetgroup"
@ -940,6 +941,35 @@ var expectedConf = &Config{
},
},
},
{
JobName: "ovhcloud",
HonorTimestamps: true,
ScrapeInterval: model.Duration(15 * time.Second),
ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
HTTPClientConfig: config.DefaultHTTPClientConfig,
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
ServiceDiscoveryConfigs: discovery.Configs{
&ovhcloud.SDConfig{
Endpoint: "ovh-eu",
ApplicationKey: "testAppKey",
ApplicationSecret: "testAppSecret",
ConsumerKey: "testConsumerKey",
RefreshInterval: model.Duration(60 * time.Second),
Service: "vps",
},
&ovhcloud.SDConfig{
Endpoint: "ovh-eu",
ApplicationKey: "testAppKey",
ApplicationSecret: "testAppSecret",
ConsumerKey: "testConsumerKey",
RefreshInterval: model.Duration(60 * time.Second),
Service: "dedicated_server",
},
},
},
{
JobName: "scaleway",
@ -1175,7 +1205,7 @@ func TestElideSecrets(t *testing.T) {
yamlConfig := string(config)
matches := secretRe.FindAllStringIndex(yamlConfig, -1)
require.Equal(t, 18, len(matches), "wrong number of secret matches found")
require.Equal(t, 22, len(matches), "wrong number of secret matches found")
require.NotContains(t, yamlConfig, "mysecret",
"yaml marshal reveals authentication credentials.")
}
@ -1618,6 +1648,14 @@ var expectedErrors = []struct {
filename: "ionos_datacenter.bad.yml",
errMsg: "datacenter id can't be empty",
},
{
filename: "ovhcloud_no_secret.bad.yml",
errMsg: "application secret can not be empty",
},
{
filename: "ovhcloud_bad_service.bad.yml",
errMsg: "unknown service: fakeservice",
},
}
func TestBadConfigs(t *testing.T) {

15
config/testdata/conf.good.yml vendored

@ -349,6 +349,21 @@ scrape_configs:
eureka_sd_configs:
- server: "http://eureka.example.com:8761/eureka"
- job_name: ovhcloud
ovhcloud_sd_configs:
- service: vps
endpoint: ovh-eu
application_key: testAppKey
application_secret: testAppSecret
consumer_key: testConsumerKey
refresh_interval: 1m
- service: dedicated_server
endpoint: ovh-eu
application_key: testAppKey
application_secret: testAppSecret
consumer_key: testConsumerKey
refresh_interval: 1m
- job_name: scaleway
scaleway_sd_configs:
- role: instance

8
config/testdata/ovhcloud_bad_service.bad.yml vendored

@ -0,0 +1,8 @@
scrape_configs:
- ovhcloud_sd_configs:
- service: fakeservice
endpoint: ovh-eu
application_key: testAppKey
application_secret: testAppSecret
consumer_key: testConsumerKey
refresh_interval: 1m

7
config/testdata/ovhcloud_no_secret.bad.yml vendored

@ -0,0 +1,7 @@
scrape_configs:
- ovhcloud_sd_configs:
- service: dedicated_server
endpoint: ovh-eu
application_key: testAppKey
consumer_key: testConsumerKey
refresh_interval: 1m

1
discovery/install/install.go

@ -33,6 +33,7 @@ import (
_ "github.com/prometheus/prometheus/discovery/moby" // register moby
_ "github.com/prometheus/prometheus/discovery/nomad" // register nomad
_ "github.com/prometheus/prometheus/discovery/openstack" // register openstack
_ "github.com/prometheus/prometheus/discovery/ovhcloud" // register ovhcloud
_ "github.com/prometheus/prometheus/discovery/puppetdb" // register puppetdb
_ "github.com/prometheus/prometheus/discovery/scaleway" // register scaleway
_ "github.com/prometheus/prometheus/discovery/triton" // register triton

161
discovery/ovhcloud/dedicated_server.go

@ -0,0 +1,161 @@
// Copyright 2021 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 ovhcloud
import (
"context"
"fmt"
"net/netip"
"net/url"
"path"
"strconv"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/ovh/go-ovh/ovh"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
const (
dedicatedServerAPIPath = "/dedicated/server"
dedicatedServerLabelPrefix = metaLabelPrefix + "dedicatedServer_"
)
type dedicatedServer struct {
State string `json:"state"`
ips []netip.Addr
CommercialRange string `json:"commercialRange"`
LinkSpeed int `json:"linkSpeed"`
Rack string `json:"rack"`
NoIntervention bool `json:"noIntervention"`
Os string `json:"os"`
SupportLevel string `json:"supportLevel"`
ServerID int64 `json:"serverId"`
Reverse string `json:"reverse"`
Datacenter string `json:"datacenter"`
Name string `json:"name"`
}
type dedicatedServerDiscovery struct {
*refresh.Discovery
config *SDConfig
logger log.Logger
}
func newDedicatedServerDiscovery(conf *SDConfig, logger log.Logger) *dedicatedServerDiscovery {
return &dedicatedServerDiscovery{config: conf, logger: logger}
}
func getDedicatedServerList(client *ovh.Client) ([]string, error) {
var dedicatedListName []string
err := client.Get(dedicatedServerAPIPath, &dedicatedListName)
if err != nil {
return nil, err
}
return dedicatedListName, nil
}
func getDedicatedServerDetails(client *ovh.Client, serverName string) (*dedicatedServer, error) {
var dedicatedServerDetails dedicatedServer
err := client.Get(path.Join(dedicatedServerAPIPath, url.QueryEscape(serverName)), &dedicatedServerDetails)
if err != nil {
return nil, err
}
var ips []string
err = client.Get(path.Join(dedicatedServerAPIPath, url.QueryEscape(serverName), "ips"), &ips)
if err != nil {
return nil, err
}
parsedIPs, err := parseIPList(ips)
if err != nil {
return nil, err
}
dedicatedServerDetails.ips = parsedIPs
return &dedicatedServerDetails, nil
}
func (d *dedicatedServerDiscovery) getService() string {
return "dedicated_server"
}
func (d *dedicatedServerDiscovery) getSource() string {
return fmt.Sprintf("%s_%s", d.config.Name(), d.getService())
}
func (d *dedicatedServerDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
client, err := createClient(d.config)
if err != nil {
return nil, err
}
var dedicatedServerDetailedList []dedicatedServer
dedicatedServerList, err := getDedicatedServerList(client)
if err != nil {
return nil, err
}
for _, dedicatedServerName := range dedicatedServerList {
dedicatedServer, err := getDedicatedServerDetails(client, dedicatedServerName)
if err != nil {
err := level.Warn(d.logger).Log("msg", fmt.Sprintf("%s: Could not get details of %s", d.getSource(), dedicatedServerName), "err", err.Error())
if err != nil {
return nil, err
}
continue
}
dedicatedServerDetailedList = append(dedicatedServerDetailedList, *dedicatedServer)
}
var targets []model.LabelSet
for _, server := range dedicatedServerDetailedList {
var ipv4, ipv6 string
for _, ip := range server.ips {
if ip.Is4() {
ipv4 = ip.String()
}
if ip.Is6() {
ipv6 = ip.String()
}
}
defaultIP := ipv4
if defaultIP == "" {
defaultIP = ipv6
}
labels := model.LabelSet{
model.AddressLabel: model.LabelValue(defaultIP),
model.InstanceLabel: model.LabelValue(server.Name),
dedicatedServerLabelPrefix + "state": model.LabelValue(server.State),
dedicatedServerLabelPrefix + "commercialRange": model.LabelValue(server.CommercialRange),
dedicatedServerLabelPrefix + "linkSpeed": model.LabelValue(fmt.Sprintf("%d", server.LinkSpeed)),
dedicatedServerLabelPrefix + "rack": model.LabelValue(server.Rack),
dedicatedServerLabelPrefix + "noIntervention": model.LabelValue(strconv.FormatBool(server.NoIntervention)),
dedicatedServerLabelPrefix + "os": model.LabelValue(server.Os),
dedicatedServerLabelPrefix + "supportLevel": model.LabelValue(server.SupportLevel),
dedicatedServerLabelPrefix + "serverId": model.LabelValue(fmt.Sprintf("%d", server.ServerID)),
dedicatedServerLabelPrefix + "reverse": model.LabelValue(server.Reverse),
dedicatedServerLabelPrefix + "datacenter": model.LabelValue(server.Datacenter),
dedicatedServerLabelPrefix + "name": model.LabelValue(server.Name),
dedicatedServerLabelPrefix + "ipv4": model.LabelValue(ipv4),
dedicatedServerLabelPrefix + "ipv6": model.LabelValue(ipv6),
}
targets = append(targets, labels)
}
return []*targetgroup.Group{{Source: d.getSource(), Targets: targets}}, nil
}

123
discovery/ovhcloud/dedicated_server_test.go

@ -0,0 +1,123 @@
// Copyright 2021 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 ovhcloud
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/go-kit/log"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
func TestOvhcloudDedicatedServerRefresh(t *testing.T) {
var cfg SDConfig
mock := httptest.NewServer(http.HandlerFunc(MockDedicatedAPI))
defer mock.Close()
cfgString := fmt.Sprintf(`
---
service: dedicated_server
endpoint: %s
application_key: %s
application_secret: %s
consumer_key: %s`, mock.URL, ovhcloudApplicationKeyTest, ovhcloudApplicationSecretTest, ovhcloudConsumerKeyTest)
require.NoError(t, yaml.UnmarshalStrict([]byte(cfgString), &cfg))
d, err := newRefresher(&cfg, log.NewNopLogger())
require.NoError(t, err)
ctx := context.Background()
targetGroups, err := d.refresh(ctx)
require.NoError(t, err)
require.Equal(t, 1, len(targetGroups))
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup)
require.NotNil(t, targetGroup.Targets)
require.Equal(t, 1, len(targetGroup.Targets))
for i, lbls := range []model.LabelSet{
{
"__address__": "1.2.3.4",
"__meta_ovhcloud_dedicatedServer_commercialRange": "Advance-1 Gen 2",
"__meta_ovhcloud_dedicatedServer_datacenter": "gra3",
"__meta_ovhcloud_dedicatedServer_ipv4": "1.2.3.4",
"__meta_ovhcloud_dedicatedServer_ipv6": "",
"__meta_ovhcloud_dedicatedServer_linkSpeed": "123",
"__meta_ovhcloud_dedicatedServer_name": "abcde",
"__meta_ovhcloud_dedicatedServer_noIntervention": "false",
"__meta_ovhcloud_dedicatedServer_os": "debian11_64",
"__meta_ovhcloud_dedicatedServer_rack": "TESTRACK",
"__meta_ovhcloud_dedicatedServer_reverse": "abcde-rev",
"__meta_ovhcloud_dedicatedServer_serverId": "1234",
"__meta_ovhcloud_dedicatedServer_state": "test",
"__meta_ovhcloud_dedicatedServer_supportLevel": "pro",
"instance": "abcde",
},
} {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
require.Equal(t, lbls, targetGroup.Targets[i])
})
}
}
func MockDedicatedAPI(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Ovh-Application") != ovhcloudApplicationKeyTest {
http.Error(w, "bad application key", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if string(r.URL.Path) == "/dedicated/server" {
dedicatedServersList, err := os.ReadFile("testdata/dedicated_server/dedicated_servers.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServersList)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if string(r.URL.Path) == "/dedicated/server/abcde" {
dedicatedServer, err := os.ReadFile("testdata/dedicated_server/dedicated_servers_details.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if string(r.URL.Path) == "/dedicated/server/abcde/ips" {
dedicatedServerIPs, err := os.ReadFile("testdata/dedicated_server/dedicated_servers_abcde_ips.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServerIPs)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

155
discovery/ovhcloud/ovhcloud.go

@ -0,0 +1,155 @@
// Copyright 2021 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 ovhcloud
import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"github.com/go-kit/log"
"github.com/ovh/go-ovh/ovh"
"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"
)
// metaLabelPrefix is the meta prefix used for all meta labels in this discovery.
const metaLabelPrefix = model.MetaLabelPrefix + "ovhcloud_"
type refresher interface {
refresh(context.Context) ([]*targetgroup.Group, error)
}
var DefaultSDConfig = SDConfig{
Endpoint: "ovh-eu",
RefreshInterval: model.Duration(60 * time.Second),
}
// SDConfig defines the Service Discovery struct used for configuration.
type SDConfig struct {
Endpoint string `yaml:"endpoint"`
ApplicationKey string `yaml:"application_key"`
ApplicationSecret config.Secret `yaml:"application_secret"`
ConsumerKey config.Secret `yaml:"consumer_key"`
RefreshInterval model.Duration `yaml:"refresh_interval"`
Service string `yaml:"service"`
}
// Name implements the Discoverer interface.
func (c SDConfig) Name() string {
return "ovhcloud"
}
// 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.Endpoint == "" {
return errors.New("endpoint can not be empty")
}
if c.ApplicationKey == "" {
return errors.New("application key can not be empty")
}
if c.ApplicationSecret == "" {
return errors.New("application secret can not be empty")
}
if c.ConsumerKey == "" {
return errors.New("consumer key can not be empty")
}
switch c.Service {
case "dedicated_server", "vps":
return nil
default:
return fmt.Errorf("unknown service: %v", c.Service)
}
}
// CreateClient creates a new ovh client configured with given credentials.
func createClient(config *SDConfig) (*ovh.Client, error) {
return ovh.NewClient(config.Endpoint, config.ApplicationKey, string(config.ApplicationSecret), string(config.ConsumerKey))
}
// NewDiscoverer new discoverer
func (c *SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
return NewDiscovery(c, options.Logger)
}
func init() {
discovery.RegisterConfig(&SDConfig{})
}
// ParseIPList parses ip list as they can have different formats.
func parseIPList(ipList []string) ([]netip.Addr, error) {
var IPs []netip.Addr
for _, ip := range ipList {
ipAddr, err := netip.ParseAddr(ip)
if err != nil {
ipPrefix, err := netip.ParsePrefix(ip)
if err != nil {
return nil, errors.New("could not parse IP addresses from list")
}
if ipPrefix.IsValid() {
netmask := ipPrefix.Bits()
if netmask != 32 {
continue
}
ipAddr = ipPrefix.Addr()
}
}
if ipAddr.IsValid() && !ipAddr.IsUnspecified() {
IPs = append(IPs, ipAddr)
}
}
if len(IPs) < 1 {
return nil, errors.New("could not parse IP addresses from list")
}
return IPs, nil
}
func newRefresher(conf *SDConfig, logger log.Logger) (refresher, error) {
switch conf.Service {
case "vps":
return newVpsDiscovery(conf, logger), nil
case "dedicated_server":
return newDedicatedServerDiscovery(conf, logger), nil
}
return nil, fmt.Errorf("unknown OVHcloud discovery service '%s'", conf.Service)
}
// NewDiscovery returns a new Ovhcloud Discoverer which periodically refreshes its targets.
func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error) {
r, err := newRefresher(conf, logger)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
logger,
"ovhcloud",
time.Duration(conf.RefreshInterval),
r.refresh,
), nil
}

129
discovery/ovhcloud/ovhcloud_test.go

@ -0,0 +1,129 @@
// Copyright 2021 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 ovhcloud
import (
"errors"
"fmt"
"testing"
"github.com/prometheus/common/config"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/util/testutil"
)
var (
ovhcloudApplicationKeyTest = "TDPKJdwZwAQPwKX2"
ovhcloudApplicationSecretTest = config.Secret("9ufkBmLaTQ9nz5yMUlg79taH0GNnzDjk")
ovhcloudConsumerKeyTest = config.Secret("5mBuy6SUQcRw2ZUxg0cG68BoDKpED4KY")
)
const (
mockURL = "https://localhost:1234"
)
func getMockConf(service string) (SDConfig, error) {
confString := fmt.Sprintf(`
endpoint: %s
application_key: %s
application_secret: %s
consumer_key: %s
refresh_interval: 1m
service: %s
`, mockURL, ovhcloudApplicationKeyTest, ovhcloudApplicationSecretTest, ovhcloudConsumerKeyTest, service)
return getMockConfFromString(confString)
}
func getMockConfFromString(confString string) (SDConfig, error) {
var conf SDConfig
err := yaml.UnmarshalStrict([]byte(confString), &conf)
return conf, err
}
func TestErrorInitClient(t *testing.T) {
confString := fmt.Sprintf(`
endpoint: %s
`, mockURL)
conf, _ := getMockConfFromString(confString)
_, err := createClient(&conf)
require.ErrorContains(t, err, "missing application key")
}
func TestParseIPs(t *testing.T) {
testCases := []struct {
name string
input []string
want error
}{
{
name: "Parse IPv4 failed.",
input: []string{"A.b"},
want: errors.New("could not parse IP addresses from list"),
},
{
name: "Parse unspecified failed.",
input: []string{"0.0.0.0"},
want: errors.New("could not parse IP addresses from list"),
},
{
name: "Parse void IP failed.",
input: []string{""},
want: errors.New("could not parse IP addresses from list"),
},
{
name: "Parse IPv6 ok.",
input: []string{"2001:0db8:0000:0000:0000:0000:0000:0001"},
want: nil,
},
{
name: "Parse IPv6 failed.",
input: []string{"bbb:cccc:1111"},
want: errors.New("could not parse IP addresses from list"),
},
{
name: "Parse IPv4 bad mask.",
input: []string{"192.0.2.1/23"},
want: errors.New("could not parse IP addresses from list"),
},
{
name: "Parse IPv4 ok.",
input: []string{"192.0.2.1/32"},
want: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := parseIPList(tc.input)
require.Equal(t, tc.want, err)
})
}
}
func TestDiscoverer(t *testing.T) {
conf, _ := getMockConf("vps")
logger := testutil.NewLogger(t)
_, err := conf.NewDiscoverer(discovery.DiscovererOptions{
Logger: logger,
})
require.NoError(t, err)
}

3
discovery/ovhcloud/testdata/dedicated_server/dedicated_servers.json vendored

@ -0,0 +1,3 @@
[
"abcde"
]

4
discovery/ovhcloud/testdata/dedicated_server/dedicated_servers_abcde_ips.json vendored

@ -0,0 +1,4 @@
[
"1.2.3.4/32",
"2001:0db8:0000:0000:0000:0000:0000:0001/64"
]

20
discovery/ovhcloud/testdata/dedicated_server/dedicated_servers_details.json vendored

@ -0,0 +1,20 @@
{
"ip": "1.2.3.4",
"newUpgradeSystem": true,
"commercialRange": "Advance-1 Gen 2",
"rack": "TESTRACK",
"rescueMail": null,
"supportLevel": "pro",
"bootId": 1,
"linkSpeed": 123,
"professionalUse": false,
"monitoring": true,
"noIntervention": false,
"name": "abcde",
"rootDevice": null,
"state": "test",
"datacenter": "gra3",
"os": "debian11_64",
"reverse": "abcde-rev",
"serverId": 1234
}

3
discovery/ovhcloud/testdata/vps/vps.json vendored

@ -0,0 +1,3 @@
[
"abc"
]

4
discovery/ovhcloud/testdata/vps/vps_abc_ips.json vendored

@ -0,0 +1,4 @@
[
"192.0.2.1/32",
"2001:0db1:0000:0000:0000:0000:0000:0001/64"
]

25
discovery/ovhcloud/testdata/vps/vps_details.json vendored

@ -0,0 +1,25 @@
{
"offerType": "ssd",
"monitoringIpBlocks": [],
"displayName": "abc",
"zone": "zone",
"cluster": "cluster_test",
"slaMonitoring": false,
"name": "abc",
"vcore": 1,
"state": "running",
"keymap": null,
"netbootMode": "local",
"model": {
"name": "vps-value-1-2-40",
"availableOptions": [],
"maximumAdditionnalIp": 16,
"offer": "VPS abc",
"disk": 40,
"version": "2019v1",
"vcore": 1,
"memory": 2048,
"datacenter": []
},
"memoryLimit": 2048
}

186
discovery/ovhcloud/vps.go

@ -0,0 +1,186 @@
// Copyright 2021 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 ovhcloud
import (
"context"
"fmt"
"net/netip"
"net/url"
"path"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/ovh/go-ovh/ovh"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
const (
vpsAPIPath = "/vps"
vpsLabelPrefix = metaLabelPrefix + "vps_"
)
// Model struct from API.
type vpsModel struct {
MaximumAdditionalIP int `json:"maximumAdditionnalIp"`
Offer string `json:"offer"`
Datacenter []string `json:"datacenter"`
Vcore int `json:"vcore"`
Version string `json:"version"`
Name string `json:"name"`
Disk int `json:"disk"`
Memory int `json:"memory"`
}
// VPS struct from API.
type virtualPrivateServer struct {
ips []netip.Addr
Keymap []string `json:"keymap"`
Zone string `json:"zone"`
Model vpsModel `json:"model"`
DisplayName string `json:"displayName"`
MonitoringIPBlocks []string `json:"monitoringIpBlocks"`
Cluster string `json:"cluster"`
State string `json:"state"`
Name string `json:"name"`
NetbootMode string `json:"netbootMode"`
MemoryLimit int `json:"memoryLimit"`
OfferType string `json:"offerType"`
Vcore int `json:"vcore"`
}
type vpsDiscovery struct {
*refresh.Discovery
config *SDConfig
logger log.Logger
}
func newVpsDiscovery(conf *SDConfig, logger log.Logger) *vpsDiscovery {
return &vpsDiscovery{config: conf, logger: logger}
}
func getVpsDetails(client *ovh.Client, vpsName string) (*virtualPrivateServer, error) {
var vpsDetails virtualPrivateServer
vpsNamePath := path.Join(vpsAPIPath, url.QueryEscape(vpsName))
err := client.Get(vpsNamePath, &vpsDetails)
if err != nil {
return nil, err
}
var ips []string
err = client.Get(path.Join(vpsNamePath, "ips"), &ips)
if err != nil {
return nil, err
}
parsedIPs, err := parseIPList(ips)
if err != nil {
return nil, err
}
vpsDetails.ips = parsedIPs
return &vpsDetails, nil
}
func getVpsList(client *ovh.Client) ([]string, error) {
var vpsListName []string
err := client.Get(vpsAPIPath, &vpsListName)
if err != nil {
return nil, err
}
return vpsListName, nil
}
func (d *vpsDiscovery) getService() string {
return "vps"
}
func (d *vpsDiscovery) getSource() string {
return fmt.Sprintf("%s_%s", d.config.Name(), d.getService())
}
func (d *vpsDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
client, err := createClient(d.config)
if err != nil {
return nil, err
}
var vpsDetailedList []virtualPrivateServer
vpsList, err := getVpsList(client)
if err != nil {
return nil, err
}
for _, vpsName := range vpsList {
vpsDetailed, err := getVpsDetails(client, vpsName)
if err != nil {
err := level.Warn(d.logger).Log("msg", fmt.Sprintf("%s: Could not get details of %s", d.getSource(), vpsName), "err", err.Error())
if err != nil {
return nil, err
}
continue
}
vpsDetailedList = append(vpsDetailedList, *vpsDetailed)
}
var targets []model.LabelSet
for _, server := range vpsDetailedList {
var ipv4, ipv6 string
for _, ip := range server.ips {
if ip.Is4() {
ipv4 = ip.String()
}
if ip.Is6() {
ipv6 = ip.String()
}
}
defaultIP := ipv4
if defaultIP == "" {
defaultIP = ipv6
}
labels := model.LabelSet{
model.AddressLabel: model.LabelValue(defaultIP),
model.InstanceLabel: model.LabelValue(server.Name),
vpsLabelPrefix + "offer": model.LabelValue(server.Model.Offer),
vpsLabelPrefix + "datacenter": model.LabelValue(fmt.Sprintf("%+v", server.Model.Datacenter)),
vpsLabelPrefix + "model_vcore": model.LabelValue(fmt.Sprintf("%d", server.Model.Vcore)),
vpsLabelPrefix + "maximumAdditionalIp": model.LabelValue(fmt.Sprintf("%d", server.Model.MaximumAdditionalIP)),
vpsLabelPrefix + "version": model.LabelValue(server.Model.Version),
vpsLabelPrefix + "model_name": model.LabelValue(server.Model.Name),
vpsLabelPrefix + "disk": model.LabelValue(fmt.Sprintf("%d", server.Model.Disk)),
vpsLabelPrefix + "memory": model.LabelValue(fmt.Sprintf("%d", server.Model.Memory)),
vpsLabelPrefix + "zone": model.LabelValue(server.Zone),
vpsLabelPrefix + "displayName": model.LabelValue(server.DisplayName),
vpsLabelPrefix + "cluster": model.LabelValue(server.Cluster),
vpsLabelPrefix + "state": model.LabelValue(server.State),
vpsLabelPrefix + "name": model.LabelValue(server.Name),
vpsLabelPrefix + "netbootMode": model.LabelValue(server.NetbootMode),
vpsLabelPrefix + "memoryLimit": model.LabelValue(fmt.Sprintf("%d", server.MemoryLimit)),
vpsLabelPrefix + "offerType": model.LabelValue(server.OfferType),
vpsLabelPrefix + "vcore": model.LabelValue(fmt.Sprintf("%d", server.Vcore)),
vpsLabelPrefix + "ipv4": model.LabelValue(ipv4),
vpsLabelPrefix + "ipv6": model.LabelValue(ipv6),
}
targets = append(targets, labels)
}
return []*targetgroup.Group{{Source: d.getSource(), Targets: targets}}, nil
}

130
discovery/ovhcloud/vps_test.go

@ -0,0 +1,130 @@
// Copyright 2021 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 ovhcloud
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
yaml "gopkg.in/yaml.v2"
"github.com/go-kit/log"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)
func TestOvhCloudVpsRefresh(t *testing.T) {
var cfg SDConfig
mock := httptest.NewServer(http.HandlerFunc(MockVpsAPI))
defer mock.Close()
cfgString := fmt.Sprintf(`
---
service: vps
endpoint: %s
application_key: %s
application_secret: %s
consumer_key: %s`, mock.URL, ovhcloudApplicationKeyTest, ovhcloudApplicationSecretTest, ovhcloudConsumerKeyTest)
require.NoError(t, yaml.UnmarshalStrict([]byte(cfgString), &cfg))
d, err := newRefresher(&cfg, log.NewNopLogger())
require.NoError(t, err)
ctx := context.Background()
targetGroups, err := d.refresh(ctx)
require.NoError(t, err)
require.Equal(t, 1, len(targetGroups))
targetGroup := targetGroups[0]
require.NotNil(t, targetGroup)
require.NotNil(t, targetGroup.Targets)
require.Equal(t, 1, len(targetGroup.Targets))
for i, lbls := range []model.LabelSet{
{
"__address__": "192.0.2.1",
"__meta_ovhcloud_vps_ipv4": "192.0.2.1",
"__meta_ovhcloud_vps_ipv6": "",
"__meta_ovhcloud_vps_cluster": "cluster_test",
"__meta_ovhcloud_vps_datacenter": "[]",
"__meta_ovhcloud_vps_disk": "40",
"__meta_ovhcloud_vps_displayName": "abc",
"__meta_ovhcloud_vps_maximumAdditionalIp": "16",
"__meta_ovhcloud_vps_memory": "2048",
"__meta_ovhcloud_vps_memoryLimit": "2048",
"__meta_ovhcloud_vps_model_name": "vps-value-1-2-40",
"__meta_ovhcloud_vps_name": "abc",
"__meta_ovhcloud_vps_netbootMode": "local",
"__meta_ovhcloud_vps_offer": "VPS abc",
"__meta_ovhcloud_vps_offerType": "ssd",
"__meta_ovhcloud_vps_state": "running",
"__meta_ovhcloud_vps_vcore": "1",
"__meta_ovhcloud_vps_model_vcore": "1",
"__meta_ovhcloud_vps_version": "2019v1",
"__meta_ovhcloud_vps_zone": "zone",
"instance": "abc",
},
} {
t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) {
require.Equal(t, lbls, targetGroup.Targets[i])
})
}
}
func MockVpsAPI(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Ovh-Application") != ovhcloudApplicationKeyTest {
http.Error(w, "bad application key", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if string(r.URL.Path) == "/vps" {
dedicatedServersList, err := os.ReadFile("testdata/vps/vps.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServersList)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if string(r.URL.Path) == "/vps/abc" {
dedicatedServer, err := os.ReadFile("testdata/vps/vps_details.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if string(r.URL.Path) == "/vps/abc/ips" {
dedicatedServerIPs, err := os.ReadFile("testdata/vps/vps_abc_ips.json")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(dedicatedServerIPs)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

70
docs/configuration/configuration.md

@ -294,6 +294,10 @@ nomad_sd_configs:
openstack_sd_configs:
[ - <openstack_sd_config> ... ]
# List of OVHcloud service discovery configurations.
ovhcloud_sd_configs:
[ - <ovhcloud_sd_config> ... ]
# List of PuppetDB service discovery configurations.
puppetdb_sd_configs:
[ - <puppetdb_sd_config> ... ]
@ -1176,6 +1180,68 @@ tls_config:
[ <tls_config> ]
```
### `<ovhcloud_sd_config>`
OVHcloud SD configurations allow retrieving scrape targets from OVHcloud's [dedicated servers](https://www.ovhcloud.com/en/bare-metal/) and [VPS](https://www.ovhcloud.com/en/vps/) using
their [API](https://api.ovh.com/).
Prometheus will periodically check the REST endpoint and create a target for every discovered server.
The role will try to use the public IPv4 address as default address, if there's none it will try to use the IPv6 one. This may be changed with relabeling.
For OVHcloud's [public cloud instances](https://www.ovhcloud.com/en/public-cloud/) you can use the [openstack_sd_config](#openstack_sd_config).
#### VPS
* `__meta_ovhcloud_vps_ipv4`: the ipv4 of the server
* `__meta_ovhcloud_vps_ipv6`: the ipv6 of the server
* `__meta_ovhcloud_vps_keymap`: the KVM keyboard layout on VPS Cloud
* `__meta_ovhcloud_vps_zone`: the zone of the server
* `__meta_ovhcloud_vps_maximumAdditionalIp`: the maximumAdditionalIp of the server
* `__meta_ovhcloud_vps_offer`: the offer of the server
* `__meta_ovhcloud_vps_datacenter`: the datacenter of the server
* `__meta_ovhcloud_vps_vcore`: the vcore of the server
* `__meta_ovhcloud_vps_version`: the version of the server
* `__meta_ovhcloud_vps_name`: the name of the server
* `__meta_ovhcloud_vps_disk`: the disk of the server
* `__meta_ovhcloud_vps_memory`: the memory of the server
* `__meta_ovhcloud_vps_displayName`: the name displayed in ManagerV6 for your VPS
* `__meta_ovhcloud_vps_monitoringIpBlocks`: the Ip blocks for OVH monitoring servers
* `__meta_ovhcloud_vps_cluster`: the cluster of the server
* `__meta_ovhcloud_vps_state`: the state of the server
* `__meta_ovhcloud_vps_name`: the name of the server
* `__meta_ovhcloud_vps_netbootMode`: the netbootMode of the server
* `__meta_ovhcloud_vps_memoryLimit`: the memoryLimit of the server
* `__meta_ovhcloud_vps_offerType`: the offerType of the server
* `__meta_ovhcloud_vps_vcore`: the vcore of the server
#### Dedicated servers
* `__meta_ovhcloud_dedicated_server_state`: the state of the server
* `__meta_ovhcloud_dedicated_server_ipv4`: the ipv4 of the server
* `__meta_ovhcloud_dedicated_server_ipv6`: the ipv6 of the server
* `__meta_ovhcloud_dedicated_server_commercialRange`: the dedicated server commercial range
* `__meta_ovhcloud_dedicated_server_linkSpeed`: the linkSpeed of the server
* `__meta_ovhcloud_dedicated_server_rack`: the rack of the server
* `__meta_ovhcloud_dedicated_server_os`: operating system
* `__meta_ovhcloud_dedicated_server_supportLevel`: the supportLevel of the server
* `__meta_ovhcloud_dedicated_server_serverId`: your server id
* `__meta_ovhcloud_dedicated_server_reverse`: dedicated server reverse
* `__meta_ovhcloud_dedicated_server_datacenter`: the dedicated datacenter localisation
* `__meta_ovhcloud_dedicated_server_name`: the dedicated server name
See below for the configuration options for OVHcloud discovery:
```yaml
# Access key to use. https://api.ovh.com
application_key: <string>
application_secret: <secret>
consumer_key: <secret>
# Service of the targets to retrieve. Must be `vps` or `dedicated_server`.
service: <string>
# API endpoint. https://github.com/ovh/go-ovh#supported-apis
[ endpoint: <string> | default = "ovh-eu" ]
# Refresh interval to re-read the resources list.
[ refresh_interval: <duration> | default = 60s ]
```
### `<puppetdb_sd_config>`
PuppetDB SD configurations allow retrieving scrape targets from
@ -2965,6 +3031,10 @@ nomad_sd_configs:
openstack_sd_configs:
[ - <openstack_sd_config> ... ]
# List of OVHcloud service discovery configurations.
ovhcloud_sd_configs:
[ - <ovhcloud_sd_config> ... ]
# List of PuppetDB service discovery configurations.
puppetdb_sd_configs:
[ - <puppetdb_sd_config> ... ]

16
documentation/examples/prometheus-ovhcloud.yml

@ -0,0 +1,16 @@
# An example scrape configuration for running Prometheus with Ovhcloud.
scrape_configs:
- job_name: 'ovhcloud'
ovhcloud_sd_configs:
- service: vps
endpoint: ovh-eu
application_key: XXX
application_secret: XXX
consumer_key: XXX
refresh_interval: 1m
- service: dedicated_server
endpoint: ovh-eu
application_key: XXX
application_secret: XXX
consumer_key: XXX
refresh_interval: 1m

1
go.mod

@ -38,6 +38,7 @@ require (
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
github.com/oklog/run v1.1.0
github.com/oklog/ulid v1.3.1
github.com/ovh/go-ovh v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/alertmanager v0.24.0
github.com/prometheus/client_golang v1.13.0

3
go.sum

@ -677,6 +677,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
@ -1445,6 +1447,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

1
plugins.yml

@ -13,6 +13,7 @@
- github.com/prometheus/prometheus/discovery/moby
- github.com/prometheus/prometheus/discovery/nomad
- github.com/prometheus/prometheus/discovery/openstack
- github.com/prometheus/prometheus/discovery/ovhcloud
- github.com/prometheus/prometheus/discovery/puppetdb
- github.com/prometheus/prometheus/discovery/scaleway
- github.com/prometheus/prometheus/discovery/triton

3
plugins/plugins.go

@ -61,6 +61,9 @@ import (
// Register openstack plugin.
_ "github.com/prometheus/prometheus/discovery/openstack"
// Register ovhcloud plugin.
_ "github.com/prometheus/prometheus/discovery/ovhcloud"
// Register puppetdb plugin.
_ "github.com/prometheus/prometheus/discovery/puppetdb"

Loading…
Cancel
Save