// Copyright 2017 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 openstack import ( "context" "fmt" "net" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/pagination" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/discovery/targetgroup" "github.com/prometheus/prometheus/util/strutil" ) const ( openstackLabelPrefix = model.MetaLabelPrefix + "openstack_" openstackLabelInstanceID = openstackLabelPrefix + "instance_id" openstackLabelInstanceName = openstackLabelPrefix + "instance_name" openstackLabelInstanceStatus = openstackLabelPrefix + "instance_status" openstackLabelInstanceFlavor = openstackLabelPrefix + "instance_flavor" openstackLabelPublicIP = openstackLabelPrefix + "public_ip" openstackLabelPrivateIP = openstackLabelPrefix + "private_ip" openstackLabelTagPrefix = openstackLabelPrefix + "tag_" ) // InstanceDiscovery discovers OpenStack instances. type InstanceDiscovery struct { authOpts *gophercloud.AuthOptions region string interval time.Duration logger log.Logger port int } // NewInstanceDiscovery returns a new instance discovery. func NewInstanceDiscovery(opts *gophercloud.AuthOptions, interval time.Duration, port int, region string, l log.Logger) *InstanceDiscovery { if l == nil { l = log.NewNopLogger() } return &InstanceDiscovery{authOpts: opts, region: region, interval: interval, port: port, logger: l} } // Run implements the Discoverer interface. func (i *InstanceDiscovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { // Get an initial set right away. tg, err := i.refresh() if err != nil { level.Error(i.logger).Log("msg", "Unable to refresh target groups", "err", err.Error()) } else { select { case ch <- []*targetgroup.Group{tg}: case <-ctx.Done(): return } } ticker := time.NewTicker(i.interval) defer ticker.Stop() for { select { case <-ticker.C: tg, err := i.refresh() if err != nil { level.Error(i.logger).Log("msg", "Unable to refresh target groups", "err", err.Error()) continue } select { case ch <- []*targetgroup.Group{tg}: case <-ctx.Done(): return } case <-ctx.Done(): return } } } func (i *InstanceDiscovery) refresh() (*targetgroup.Group, error) { var err error t0 := time.Now() defer func() { refreshDuration.Observe(time.Since(t0).Seconds()) if err != nil { refreshFailuresCount.Inc() } }() provider, err := openstack.AuthenticatedClient(*i.authOpts) if err != nil { return nil, fmt.Errorf("could not create OpenStack session: %s", err) } client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ Region: i.region, }) if err != nil { return nil, fmt.Errorf("could not create OpenStack compute session: %s", err) } // OpenStack API reference // https://developer.openstack.org/api-ref/compute/#list-floating-ips pagerFIP := floatingips.List(client) floatingIPList := make(map[string][]string) err = pagerFIP.EachPage(func(page pagination.Page) (bool, error) { result, err := floatingips.ExtractFloatingIPs(page) if err != nil { return false, fmt.Errorf("could not extract floatingips: %s", err) } for _, ip := range result { // Skip not associated ips if ip.InstanceID != "" { floatingIPList[ip.InstanceID] = append(floatingIPList[ip.InstanceID], ip.IP) } } return true, nil }) if err != nil { return nil, err } // OpenStack API reference // https://developer.openstack.org/api-ref/compute/#list-servers opts := servers.ListOpts{} pager := servers.List(client, opts) tg := &targetgroup.Group{ Source: fmt.Sprintf("OS_" + i.region), } err = pager.EachPage(func(page pagination.Page) (bool, error) { instanceList, err := servers.ExtractServers(page) if err != nil { return false, fmt.Errorf("could not extract instances: %s", err) } for _, s := range instanceList { labels := model.LabelSet{ openstackLabelInstanceID: model.LabelValue(s.ID), } if len(s.Addresses) == 0 { level.Info(i.logger).Log("msg", "Got no IP address", "instance", s.ID) continue } for _, address := range s.Addresses { md, ok := address.([]interface{}) if !ok { level.Warn(i.logger).Log("msg", "Invalid type for address, expected array") continue } if len(md) == 0 { level.Debug(i.logger).Log("msg", "Got no IP address", "instance", s.ID) continue } md1, ok := md[0].(map[string]interface{}) if !ok { level.Warn(i.logger).Log("msg", "Invalid type for address, expected dict") continue } addr, ok := md1["addr"].(string) if !ok { level.Warn(i.logger).Log("msg", "Invalid type for address, expected string") continue } labels[openstackLabelPrivateIP] = model.LabelValue(addr) addr = net.JoinHostPort(addr, fmt.Sprintf("%d", i.port)) labels[model.AddressLabel] = model.LabelValue(addr) // Only use first private IP break } if val, ok := floatingIPList[s.ID]; ok && len(val) > 0 { labels[openstackLabelPublicIP] = model.LabelValue(val[0]) } labels[openstackLabelInstanceStatus] = model.LabelValue(s.Status) labels[openstackLabelInstanceName] = model.LabelValue(s.Name) id, ok := s.Flavor["id"].(string) if !ok { level.Warn(i.logger).Log("msg", "Invalid type for instance id, excepted string") continue } labels[openstackLabelInstanceFlavor] = model.LabelValue(id) for k, v := range s.Metadata { name := strutil.SanitizeLabelName(k) labels[openstackLabelTagPrefix+model.LabelName(name)] = model.LabelValue(v) } tg.Targets = append(tg.Targets, labels) } return true, nil }) if err != nil { return nil, err } return tg, nil }