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