elasticapm/utils/cloud.py (110 lines of code) (raw):
# BSD 3-Clause License
#
# Copyright (c) 2019, Elasticsearch BV
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import json
import os
import socket
import urllib3
def aws_metadata():
"""
Fetch AWS metadata from the local metadata server. If metadata server is
not found, return an empty dictionary
"""
http = urllib3.PoolManager()
try:
# This will throw an error if the metadata server isn't available,
# and will be quiet in the logs, unlike urllib3
with socket.create_connection(("169.254.169.254", 80), 0.1):
pass
try:
# This whole block is almost unnecessary. IMDSv1 will be supported
# indefinitely, so the only time this block is needed is if a
# security-conscious user has set the metadata service to require
# IMDSv2. Thus, the very expansive try:except: coverage.
# TODO: should we have a config option to completely disable IMDSv2 to reduce overhead?
ttl_header = {"X-aws-ec2-metadata-token-ttl-seconds": "300"}
token_url = "http://169.254.169.254/latest/api/token"
token_request = http.request("PUT", token_url, headers=ttl_header, timeout=1.0, retries=False)
token = token_request.data.decode("utf-8")
aws_token_header = {"X-aws-ec2-metadata-token": token} if token else {}
except Exception:
aws_token_header = {}
metadata = json.loads(
http.request(
"GET",
"http://169.254.169.254/latest/dynamic/instance-identity/document",
headers=aws_token_header,
timeout=1.0,
retries=False,
).data.decode("utf-8")
)
return {
"account": {"id": metadata["accountId"]},
"instance": {"id": metadata["instanceId"]},
"availability_zone": metadata["availabilityZone"],
"machine": {"type": metadata["instanceType"]},
"provider": "aws",
"region": metadata["region"],
}
except Exception:
# Not on an AWS box
return {}
def gcp_metadata():
"""
Fetch GCP metadata from the local metadata server. If metadata server is
not found, return an empty dictionary
"""
headers = {"Metadata-Flavor": "Google"}
http = urllib3.PoolManager()
try:
# This will throw an error if the metadata server isn't available,
# and will be quiet in the logs, unlike urllib3
socket.getaddrinfo("metadata.google.internal", 80, 0, socket.SOCK_STREAM)
metadata = json.loads(
http.request(
"GET",
"http://metadata.google.internal/computeMetadata/v1/?recursive=true",
headers=headers,
timeout=1.0,
retries=False,
).data.decode("utf-8")
)
availability_zone = os.path.split(metadata["instance"]["zone"])[1]
return {
"provider": "gcp",
"instance": {"id": str(metadata["instance"]["id"]), "name": metadata["instance"]["name"]},
"project": {"id": metadata["project"]["projectId"]},
"availability_zone": availability_zone,
"region": availability_zone.rsplit("-", 1)[0],
"machine": {"type": metadata["instance"]["machineType"].split("/")[-1]},
}
except Exception:
# Not on a gcp box
return {}
def azure_metadata():
"""
Fetch Azure metadata from the local metadata server. If metadata server is
not found, return an empty dictionary
"""
headers = {"Metadata": "true"}
http = urllib3.PoolManager()
try:
# This will throw an error if the metadata server isn't available,
# and will be quiet in the logs, unlike urllib3
with socket.create_connection(("169.254.169.254", 80), 0.1):
pass
# Can't use newest metadata service version, as it's not guaranteed
# to be available in all regions
metadata = json.loads(
http.request(
"GET",
"http://169.254.169.254/metadata/instance/compute?api-version=2019-08-15",
headers=headers,
timeout=1.0,
retries=False,
).data.decode("utf-8")
)
ret = {
"account": {"id": metadata["subscriptionId"]},
"instance": {"id": metadata["vmId"], "name": metadata["name"]},
"project": {"name": metadata["resourceGroupName"]},
"availability_zone": metadata["zone"],
"machine": {"type": metadata["vmSize"]},
"provider": "azure",
"region": metadata["location"],
}
if not ret["availability_zone"]:
ret.pop("availability_zone")
return ret
except Exception:
# Not on an Azure box, maybe an azure app service?
return azure_app_service_metadata()
def azure_app_service_metadata():
ret = {"provider": "azure"}
website_owner_name = os.environ.get("WEBSITE_OWNER_NAME")
website_instance_id = os.environ.get("WEBSITE_INSTANCE_ID")
website_site_name = os.environ.get("WEBSITE_SITE_NAME")
website_resource_group = os.environ.get("WEBSITE_RESOURCE_GROUP")
if not all((website_owner_name, website_instance_id, website_site_name, website_resource_group)):
return {}
# Format of website_owner_name: {subscription id}+{app service plan resource group}-{region}webspace{.*}
if "+" not in website_owner_name:
return {}
try:
account_id, website_owner_name = website_owner_name.split("+")
ret["account"] = {"id": account_id}
region, _ = website_owner_name.split("webspace")
ret["region"] = region.rsplit("-", 1)[1]
except Exception:
return {}
ret["instance"] = {"id": website_instance_id, "name": website_site_name}
ret["project"] = {"name": website_resource_group}
return ret