2020-04-27 16:25:45 +00:00
|
|
|
package ec2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/xml"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2021-08-03 10:26:28 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
2020-04-27 16:25:45 +00:00
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
|
|
|
|
)
|
|
|
|
|
|
|
|
// getInstancesLabels returns labels for ec2 instances obtained from the given cfg
|
|
|
|
func getInstancesLabels(cfg *apiConfig) ([]map[string]string, error) {
|
|
|
|
rs, err := getReservations(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-08-03 10:26:28 +00:00
|
|
|
azMap := getAZMap(cfg)
|
2020-04-27 16:25:45 +00:00
|
|
|
var ms []map[string]string
|
|
|
|
for _, r := range rs {
|
|
|
|
for _, inst := range r.InstanceSet.Items {
|
2021-08-03 10:26:28 +00:00
|
|
|
ms = inst.appendTargetLabels(ms, r.OwnerID, cfg.port, azMap)
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getReservations(cfg *apiConfig) ([]Reservation, error) {
|
|
|
|
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
|
|
|
|
var rs []Reservation
|
|
|
|
pageToken := ""
|
|
|
|
for {
|
2021-09-17 15:00:37 +00:00
|
|
|
data, err := getEC2APIResponse(cfg, "DescribeInstances", cfg.filters, pageToken)
|
2020-04-27 16:25:45 +00:00
|
|
|
if err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot obtain instances: %w", err)
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
ir, err := parseInstancesResponse(data)
|
|
|
|
if err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot parse instance list: %w", err)
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
rs = append(rs, ir.ReservationSet.Items...)
|
|
|
|
if len(ir.NextPageToken) == 0 {
|
|
|
|
return rs, nil
|
|
|
|
}
|
|
|
|
pageToken = ir.NextPageToken
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// InstancesResponse represents response to https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
|
|
|
|
type InstancesResponse struct {
|
|
|
|
ReservationSet ReservationSet `xml:"reservationSet"`
|
|
|
|
NextPageToken string `xml:"nextToken"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReservationSet represetns ReservationSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
|
|
|
|
type ReservationSet struct {
|
|
|
|
Items []Reservation `xml:"item"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reservation represents Reservation from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Reservation.html
|
|
|
|
type Reservation struct {
|
|
|
|
OwnerID string `xml:"ownerId"`
|
|
|
|
InstanceSet InstanceSet `xml:"instancesSet"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// InstanceSet represents InstanceSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Reservation.html
|
|
|
|
type InstanceSet struct {
|
|
|
|
Items []Instance `xml:"item"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Instance represents Instance from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html
|
|
|
|
type Instance struct {
|
|
|
|
PrivateIPAddress string `xml:"privateIpAddress"`
|
|
|
|
Architecture string `xml:"architecture"`
|
|
|
|
Placement Placement `xml:"placement"`
|
2020-06-20 14:40:47 +00:00
|
|
|
ImageID string `xml:"imageId"`
|
2020-04-27 16:25:45 +00:00
|
|
|
ID string `xml:"instanceId"`
|
|
|
|
Lifecycle string `xml:"instanceLifecycle"`
|
|
|
|
State InstanceState `xml:"instanceState"`
|
|
|
|
Type string `xml:"instanceType"`
|
|
|
|
Platform string `xml:"platform"`
|
|
|
|
SubnetID string `xml:"subnetId"`
|
|
|
|
PrivateDNSName string `xml:"privateDnsName"`
|
|
|
|
PublicDNSName string `xml:"dnsName"`
|
|
|
|
PublicIPAddress string `xml:"ipAddress"`
|
|
|
|
VPCID string `xml:"vpcId"`
|
|
|
|
NetworkInterfaceSet NetworkInterfaceSet `xml:"networkInterfaceSet"`
|
|
|
|
TagSet TagSet `xml:"tagSet"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Placement represents Placement from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Placement.html
|
|
|
|
type Placement struct {
|
|
|
|
AvailabilityZone string `xml:"availabilityZone"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// InstanceState represents InstanceState from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceState.html
|
|
|
|
type InstanceState struct {
|
|
|
|
Name string `xml:"name"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetworkInterfaceSet represents NetworkInterfaceSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html
|
|
|
|
type NetworkInterfaceSet struct {
|
|
|
|
Items []NetworkInterface `xml:"item"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetworkInterface represents NetworkInterface from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceNetworkInterface.html
|
|
|
|
type NetworkInterface struct {
|
2020-11-23 14:52:53 +00:00
|
|
|
SubnetID string `xml:"subnetId"`
|
|
|
|
IPv6AddressesSet Ipv6AddressesSet `xml:"ipv6AddressesSet"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ipv6AddressesSet represents ipv6AddressesSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceNetworkInterface.html
|
|
|
|
type Ipv6AddressesSet struct {
|
|
|
|
Items []string `xml:"item"`
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TagSet represents TagSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html
|
|
|
|
type TagSet struct {
|
|
|
|
Items []Tag `xml:"item"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tag represents Tag from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Tag.html
|
|
|
|
type Tag struct {
|
|
|
|
Key string `xml:"key"`
|
|
|
|
Value string `xml:"value"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseInstancesResponse(data []byte) (*InstancesResponse, error) {
|
|
|
|
var v InstancesResponse
|
|
|
|
if err := xml.Unmarshal(data, &v); err != nil {
|
2020-06-30 19:58:18 +00:00
|
|
|
return nil, fmt.Errorf("cannot unmarshal InstancesResponse from %q: %w", data, err)
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
return &v, nil
|
|
|
|
}
|
|
|
|
|
2021-08-03 10:26:28 +00:00
|
|
|
func getAZMap(cfg *apiConfig) map[string]string {
|
|
|
|
cfg.azMapLock.Lock()
|
|
|
|
defer cfg.azMapLock.Unlock()
|
|
|
|
|
|
|
|
if cfg.azMap != nil {
|
|
|
|
return cfg.azMap
|
|
|
|
}
|
|
|
|
|
|
|
|
azs, err := getAvailabilityZones(cfg)
|
|
|
|
cfg.azMap = make(map[string]string, len(azs))
|
|
|
|
if err != nil {
|
|
|
|
logger.Warnf("couldn't load availability zones map, so __meta_ec2_availability_zone_id label isn't set: %s", err)
|
|
|
|
return cfg.azMap
|
|
|
|
}
|
|
|
|
for _, az := range azs {
|
|
|
|
cfg.azMap[az.ZoneName] = az.ZoneID
|
|
|
|
}
|
|
|
|
return cfg.azMap
|
|
|
|
}
|
|
|
|
|
|
|
|
func getAvailabilityZones(cfg *apiConfig) ([]AvailabilityZone, error) {
|
|
|
|
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeAvailabilityZones.html
|
2021-09-17 15:00:37 +00:00
|
|
|
data, err := getEC2APIResponse(cfg, "DescribeAvailabilityZones", "", "")
|
2021-08-03 10:26:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot obtain availability zones: %w", err)
|
|
|
|
}
|
|
|
|
azr, err := parseAvailabilityZonesResponse(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot parse availability zones list: %w", err)
|
|
|
|
}
|
|
|
|
return azr.AvailabilityZoneInfo.Items, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AvailabilityZonesResponse represents the response for https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeAvailabilityZones.html
|
|
|
|
type AvailabilityZonesResponse struct {
|
|
|
|
AvailabilityZoneInfo AvailabilityZoneInfo `xml:"availabilityZoneInfo"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// AvailabilityZoneInfo represents availabilityZoneInfo for https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeAvailabilityZones.html
|
|
|
|
type AvailabilityZoneInfo struct {
|
|
|
|
Items []AvailabilityZone `xml:"item"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// AvailabilityZone represents availabilityZone for https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_AvailabilityZone.html
|
|
|
|
type AvailabilityZone struct {
|
|
|
|
ZoneName string `xml:"zoneName"`
|
|
|
|
ZoneID string `xml:"zoneId"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseAvailabilityZonesResponse(data []byte) (*AvailabilityZonesResponse, error) {
|
|
|
|
var v AvailabilityZonesResponse
|
|
|
|
if err := xml.Unmarshal(data, &v); err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot unmarshal DescribeAvailabilityZonesResponse from %q: %w", data, err)
|
|
|
|
}
|
|
|
|
return &v, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (inst *Instance) appendTargetLabels(ms []map[string]string, ownerID string, port int, azMap map[string]string) []map[string]string {
|
2020-04-27 16:25:45 +00:00
|
|
|
if len(inst.PrivateIPAddress) == 0 {
|
|
|
|
// Cannot scrape instance without private IP address
|
|
|
|
return ms
|
|
|
|
}
|
|
|
|
addr := discoveryutils.JoinHostPort(inst.PrivateIPAddress, port)
|
|
|
|
m := map[string]string{
|
2021-08-03 10:26:28 +00:00
|
|
|
"__address__": addr,
|
|
|
|
"__meta_ec2_architecture": inst.Architecture,
|
|
|
|
"__meta_ec2_ami": inst.ImageID,
|
|
|
|
"__meta_ec2_availability_zone": inst.Placement.AvailabilityZone,
|
|
|
|
"__meta_ec2_availability_zone_id": azMap[inst.Placement.AvailabilityZone],
|
|
|
|
"__meta_ec2_instance_id": inst.ID,
|
|
|
|
"__meta_ec2_instance_lifecycle": inst.Lifecycle,
|
|
|
|
"__meta_ec2_instance_state": inst.State.Name,
|
|
|
|
"__meta_ec2_instance_type": inst.Type,
|
|
|
|
"__meta_ec2_owner_id": ownerID,
|
|
|
|
"__meta_ec2_platform": inst.Platform,
|
|
|
|
"__meta_ec2_primary_subnet_id": inst.SubnetID,
|
|
|
|
"__meta_ec2_private_dns_name": inst.PrivateDNSName,
|
|
|
|
"__meta_ec2_private_ip": inst.PrivateIPAddress,
|
|
|
|
"__meta_ec2_public_dns_name": inst.PublicDNSName,
|
|
|
|
"__meta_ec2_public_ip": inst.PublicIPAddress,
|
|
|
|
"__meta_ec2_vpc_id": inst.VPCID,
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
if len(inst.VPCID) > 0 {
|
|
|
|
subnets := make([]string, 0, len(inst.NetworkInterfaceSet.Items))
|
|
|
|
seenSubnets := make(map[string]bool, len(inst.NetworkInterfaceSet.Items))
|
2020-11-23 14:52:53 +00:00
|
|
|
var ipv6Addrs []string
|
2020-04-27 16:25:45 +00:00
|
|
|
for _, ni := range inst.NetworkInterfaceSet.Items {
|
|
|
|
if len(ni.SubnetID) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2020-11-23 14:52:53 +00:00
|
|
|
// Deduplicate VPC Subnet IDs maintaining the order of the network interfaces returned by EC2.
|
2020-04-27 16:25:45 +00:00
|
|
|
if !seenSubnets[ni.SubnetID] {
|
|
|
|
seenSubnets[ni.SubnetID] = true
|
|
|
|
subnets = append(subnets, ni.SubnetID)
|
|
|
|
}
|
2020-11-23 14:52:53 +00:00
|
|
|
// Collect ipv6 addresses
|
|
|
|
ipv6Addrs = append(ipv6Addrs, ni.IPv6AddressesSet.Items...)
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
// We surround the separated list with the separator as well. This way regular expressions
|
|
|
|
// in relabeling rules don't have to consider tag positions.
|
|
|
|
m["__meta_ec2_subnet_id"] = "," + strings.Join(subnets, ",") + ","
|
2020-11-23 14:52:53 +00:00
|
|
|
if len(ipv6Addrs) > 0 {
|
|
|
|
m["__meta_ec2_ipv6_addresses"] = "," + strings.Join(ipv6Addrs, ",") + ","
|
|
|
|
}
|
2020-04-27 16:25:45 +00:00
|
|
|
}
|
|
|
|
for _, t := range inst.TagSet.Items {
|
|
|
|
if len(t.Key) == 0 || len(t.Value) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
name := discoveryutils.SanitizeLabelName(t.Key)
|
|
|
|
m["__meta_ec2_tag_"+name] = t.Value
|
|
|
|
}
|
|
|
|
ms = append(ms, m)
|
|
|
|
return ms
|
|
|
|
}
|