// 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 linode import ( "context" "fmt" "net" "net/http" "strconv" "strings" "time" "github.com/go-kit/kit/log" "github.com/linode/linodego" "github.com/prometheus/common/config" "github.com/prometheus/common/model" "github.com/prometheus/common/version" "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/discovery/refresh" "github.com/prometheus/prometheus/discovery/targetgroup" ) const ( linodeLabel = model.MetaLabelPrefix + "linode_" linodeLabelID = linodeLabel + "instance_id" linodeLabelName = linodeLabel + "instance_label" linodeLabelImage = linodeLabel + "image" linodeLabelPrivateIPv4 = linodeLabel + "private_ipv4" linodeLabelPublicIPv4 = linodeLabel + "public_ipv4" linodeLabelPublicIPv6 = linodeLabel + "public_ipv6" linodeLabelPrivateIPv4RDNS = linodeLabel + "private_ipv4_rdns" linodeLabelPublicIPv4RDNS = linodeLabel + "public_ipv4_rdns" linodeLabelPublicIPv6RDNS = linodeLabel + "public_ipv6_rdns" linodeLabelRegion = linodeLabel + "region" linodeLabelType = linodeLabel + "type" linodeLabelStatus = linodeLabel + "status" linodeLabelTags = linodeLabel + "tags" linodeLabelGroup = linodeLabel + "group" linodeLabelHypervisor = linodeLabel + "hypervisor" linodeLabelBackups = linodeLabel + "backups" linodeLabelSpecsDiskBytes = linodeLabel + "specs_disk_bytes" linodeLabelSpecsMemoryBytes = linodeLabel + "specs_memory_bytes" linodeLabelSpecsVCPUs = linodeLabel + "specs_vcpus" linodeLabelSpecsTransferBytes = linodeLabel + "specs_transfer_bytes" linodeLabelExtraIPs = linodeLabel + "extra_ips" ) // DefaultSDConfig is the default Linode SD configuration. var DefaultSDConfig = SDConfig{ TagSeparator: ",", Port: 80, RefreshInterval: model.Duration(60 * time.Second), HTTPClientConfig: config.DefaultHTTPClientConfig, } func init() { discovery.RegisterConfig(&SDConfig{}) } // SDConfig is the configuration for Linode based service discovery. type SDConfig struct { HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` RefreshInterval model.Duration `yaml:"refresh_interval"` Port int `yaml:"port"` TagSeparator string `yaml:"tag_separator,omitempty"` } // Name returns the name of the Config. func (*SDConfig) Name() string { return "linode" } // NewDiscoverer returns a Discoverer for the Config. func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { return NewDiscovery(c, opts.Logger) } // SetDirectory joins any relative file paths with dir. func (c *SDConfig) SetDirectory(dir string) { c.HTTPClientConfig.SetDirectory(dir) } // 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 } return c.HTTPClientConfig.Validate() } // Discovery periodically performs Linode requests. It implements // the Discoverer interface. type Discovery struct { *refresh.Discovery client *linodego.Client port int tagSeparator string } // NewDiscovery returns a new Discovery which periodically refreshes its targets. func NewDiscovery(conf *SDConfig, logger log.Logger) (*Discovery, error) { d := &Discovery{ port: conf.Port, tagSeparator: conf.TagSeparator, } rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "linode_sd", config.WithHTTP2Disabled()) if err != nil { return nil, err } client := linodego.NewClient( &http.Client{ Transport: rt, Timeout: time.Duration(conf.RefreshInterval), }, ) client.SetUserAgent(fmt.Sprintf("Prometheus/%s", version.Version)) d.client = &client d.Discovery = refresh.NewDiscovery( logger, "linode", time.Duration(conf.RefreshInterval), d.refresh, ) return d, nil } func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { tg := &targetgroup.Group{ Source: "Linode", } // Gather all linode instances. instances, err := d.client.ListInstances(ctx, &linodego.ListOptions{}) if err != nil { return nil, err } // Gather detailed IP address info for all IPs on all linode instances. detailedIPs, err := d.client.ListIPAddresses(ctx, &linodego.ListOptions{}) if err != nil { return nil, err } for _, instance := range instances { if len(instance.IPv4) == 0 { continue } var ( privateIPv4, publicIPv4, publicIPv6 string privateIPv4RDNS, publicIPv4RDNS, publicIPv6RDNS string backupsStatus string extraIPs []string ) for _, ip := range instance.IPv4 { for _, detailedIP := range detailedIPs { if detailedIP.Address != ip.String() { continue } if detailedIP.Public && publicIPv4 == "" { publicIPv4 = detailedIP.Address if detailedIP.RDNS != "" && detailedIP.RDNS != "null" { publicIPv4RDNS = detailedIP.RDNS } } else if !detailedIP.Public && privateIPv4 == "" { privateIPv4 = detailedIP.Address if detailedIP.RDNS != "" && detailedIP.RDNS != "null" { privateIPv4RDNS = detailedIP.RDNS } } else { extraIPs = append(extraIPs, detailedIP.Address) } } } if instance.IPv6 != "" { for _, detailedIP := range detailedIPs { if detailedIP.Address != strings.Split(instance.IPv6, "/")[0] { continue } publicIPv6 = detailedIP.Address if detailedIP.RDNS != "" && detailedIP.RDNS != "null" { publicIPv6RDNS = detailedIP.RDNS } } } if instance.Backups.Enabled { backupsStatus = "enabled" } else { backupsStatus = "disabled" } labels := model.LabelSet{ linodeLabelID: model.LabelValue(fmt.Sprintf("%d", instance.ID)), linodeLabelName: model.LabelValue(instance.Label), linodeLabelImage: model.LabelValue(instance.Image), linodeLabelPrivateIPv4: model.LabelValue(privateIPv4), linodeLabelPublicIPv4: model.LabelValue(publicIPv4), linodeLabelPublicIPv6: model.LabelValue(publicIPv6), linodeLabelPrivateIPv4RDNS: model.LabelValue(privateIPv4RDNS), linodeLabelPublicIPv4RDNS: model.LabelValue(publicIPv4RDNS), linodeLabelPublicIPv6RDNS: model.LabelValue(publicIPv6RDNS), linodeLabelRegion: model.LabelValue(instance.Region), linodeLabelType: model.LabelValue(instance.Type), linodeLabelStatus: model.LabelValue(instance.Status), linodeLabelGroup: model.LabelValue(instance.Group), linodeLabelHypervisor: model.LabelValue(instance.Hypervisor), linodeLabelBackups: model.LabelValue(backupsStatus), linodeLabelSpecsDiskBytes: model.LabelValue(fmt.Sprintf("%d", instance.Specs.Disk<<20)), linodeLabelSpecsMemoryBytes: model.LabelValue(fmt.Sprintf("%d", instance.Specs.Memory<<20)), linodeLabelSpecsVCPUs: model.LabelValue(fmt.Sprintf("%d", instance.Specs.VCPUs)), linodeLabelSpecsTransferBytes: model.LabelValue(fmt.Sprintf("%d", instance.Specs.Transfer<<20)), } addr := net.JoinHostPort(publicIPv4, strconv.FormatUint(uint64(d.port), 10)) labels[model.AddressLabel] = model.LabelValue(addr) if len(instance.Tags) > 0 { // We surround the separated list with the separator as well. This way regular expressions // in relabeling rules don't have to consider tag positions. tags := d.tagSeparator + strings.Join(instance.Tags, d.tagSeparator) + d.tagSeparator labels[linodeLabelTags] = model.LabelValue(tags) } if len(extraIPs) > 0 { // This instance has more than one of at least one type of IP address (public, private, // IPv4, IPv6, etc. We provide those extra IPs found here just like we do for instance // tags, we surround a separated list with the tagSeparator config. ips := d.tagSeparator + strings.Join(extraIPs, d.tagSeparator) + d.tagSeparator labels[linodeLabelExtraIPs] = model.LabelValue(ips) } tg.Targets = append(tg.Targets, labels) } return []*targetgroup.Group{tg}, nil }