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>
This commit is contained in:
Marine Bal 2022-11-03 10:20:09 +01:00 committed by GitHub
parent e576103d7e
commit 16c3aa75c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1107 additions and 1 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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)
}

View file

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

View file

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

View file

@ -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
}

View file

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

View file

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

View file

@ -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 Normal file
View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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> ... ]

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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

View file

@ -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"