mirror of https://github.com/prometheus/prometheus
Browse Source
* 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
23 changed files with 1107 additions and 1 deletions
@ -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 |
@ -0,0 +1,7 @@ |
|||||||
|
scrape_configs: |
||||||
|
- ovhcloud_sd_configs: |
||||||
|
- service: dedicated_server |
||||||
|
endpoint: ovh-eu |
||||||
|
application_key: testAppKey |
||||||
|
consumer_key: testConsumerKey |
||||||
|
refresh_interval: 1m |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
[ |
||||||
|
"abcde" |
||||||
|
] |
@ -0,0 +1,4 @@ |
|||||||
|
[ |
||||||
|
"1.2.3.4/32", |
||||||
|
"2001:0db8:0000:0000:0000:0000:0000:0001/64" |
||||||
|
] |
@ -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 |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
[ |
||||||
|
"192.0.2.1/32", |
||||||
|
"2001:0db1:0000:0000:0000:0000:0000:0001/64" |
||||||
|
] |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 |
Loading…
Reference in new issue