1# Copyright 2016 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Provides helper methods for talking to the Compute Engine metadata server. 16 17See https://cloud.google.com/compute/docs/metadata for more details. 18""" 19 20import datetime 21import json 22import logging 23import os 24 25import six 26from six.moves import http_client 27from six.moves.urllib import parse as urlparse 28 29from google.auth import _helpers 30from google.auth import environment_vars 31from google.auth import exceptions 32 33_LOGGER = logging.getLogger(__name__) 34 35# Environment variable GCE_METADATA_HOST is originally named 36# GCE_METADATA_ROOT. For compatiblity reasons, here it checks 37# the new variable first; if not set, the system falls back 38# to the old variable. 39_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None) 40if not _GCE_METADATA_HOST: 41 _GCE_METADATA_HOST = os.getenv( 42 environment_vars.GCE_METADATA_ROOT, "metadata.google.internal" 43 ) 44_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST) 45 46# This is used to ping the metadata server, it avoids the cost of a DNS 47# lookup. 48_METADATA_IP_ROOT = "http://{}".format( 49 os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254") 50) 51_METADATA_FLAVOR_HEADER = "metadata-flavor" 52_METADATA_FLAVOR_VALUE = "Google" 53_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE} 54 55# Timeout in seconds to wait for the GCE metadata server when detecting the 56# GCE environment. 57try: 58 _METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3)) 59except ValueError: # pragma: NO COVER 60 _METADATA_DEFAULT_TIMEOUT = 3 61 62 63def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3): 64 """Checks to see if the metadata server is available. 65 66 Args: 67 request (google.auth.transport.Request): A callable used to make 68 HTTP requests. 69 timeout (int): How long to wait for the metadata server to respond. 70 retry_count (int): How many times to attempt connecting to metadata 71 server using above timeout. 72 73 Returns: 74 bool: True if the metadata server is reachable, False otherwise. 75 """ 76 # NOTE: The explicit ``timeout`` is a workaround. The underlying 77 # issue is that resolving an unknown host on some networks will take 78 # 20-30 seconds; making this timeout short fixes the issue, but 79 # could lead to false negatives in the event that we are on GCE, but 80 # the metadata resolution was particularly slow. The latter case is 81 # "unlikely". 82 retries = 0 83 while retries < retry_count: 84 try: 85 response = request( 86 url=_METADATA_IP_ROOT, 87 method="GET", 88 headers=_METADATA_HEADERS, 89 timeout=timeout, 90 ) 91 92 metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER) 93 return ( 94 response.status == http_client.OK 95 and metadata_flavor == _METADATA_FLAVOR_VALUE 96 ) 97 98 except exceptions.TransportError as e: 99 _LOGGER.warning( 100 "Compute Engine Metadata server unavailable on " 101 "attempt %s of %s. Reason: %s", 102 retries + 1, 103 retry_count, 104 e, 105 ) 106 retries += 1 107 108 return False 109 110 111def get( 112 request, path, root=_METADATA_ROOT, params=None, recursive=False, retry_count=5 113): 114 """Fetch a resource from the metadata server. 115 116 Args: 117 request (google.auth.transport.Request): A callable used to make 118 HTTP requests. 119 path (str): The resource to retrieve. For example, 120 ``'instance/service-accounts/default'``. 121 root (str): The full path to the metadata server root. 122 params (Optional[Mapping[str, str]]): A mapping of query parameter 123 keys to values. 124 recursive (bool): Whether to do a recursive query of metadata. See 125 https://cloud.google.com/compute/docs/metadata#aggcontents for more 126 details. 127 retry_count (int): How many times to attempt connecting to metadata 128 server using above timeout. 129 130 Returns: 131 Union[Mapping, str]: If the metadata server returns JSON, a mapping of 132 the decoded JSON is return. Otherwise, the response content is 133 returned as a string. 134 135 Raises: 136 google.auth.exceptions.TransportError: if an error occurred while 137 retrieving metadata. 138 """ 139 base_url = urlparse.urljoin(root, path) 140 query_params = {} if params is None else params 141 142 if recursive: 143 query_params["recursive"] = "true" 144 145 url = _helpers.update_query(base_url, query_params) 146 147 retries = 0 148 while retries < retry_count: 149 try: 150 response = request(url=url, method="GET", headers=_METADATA_HEADERS) 151 break 152 153 except exceptions.TransportError as e: 154 _LOGGER.warning( 155 "Compute Engine Metadata server unavailable on " 156 "attempt %s of %s. Reason: %s", 157 retries + 1, 158 retry_count, 159 e, 160 ) 161 retries += 1 162 else: 163 raise exceptions.TransportError( 164 "Failed to retrieve {} from the Google Compute Engine" 165 "metadata service. Compute Engine Metadata server unavailable".format(url) 166 ) 167 168 if response.status == http_client.OK: 169 content = _helpers.from_bytes(response.data) 170 if response.headers["content-type"] == "application/json": 171 try: 172 return json.loads(content) 173 except ValueError as caught_exc: 174 new_exc = exceptions.TransportError( 175 "Received invalid JSON from the Google Compute Engine" 176 "metadata service: {:.20}".format(content) 177 ) 178 six.raise_from(new_exc, caught_exc) 179 else: 180 return content 181 else: 182 raise exceptions.TransportError( 183 "Failed to retrieve {} from the Google Compute Engine" 184 "metadata service. Status: {} Response:\n{}".format( 185 url, response.status, response.data 186 ), 187 response, 188 ) 189 190 191def get_project_id(request): 192 """Get the Google Cloud Project ID from the metadata server. 193 194 Args: 195 request (google.auth.transport.Request): A callable used to make 196 HTTP requests. 197 198 Returns: 199 str: The project ID 200 201 Raises: 202 google.auth.exceptions.TransportError: if an error occurred while 203 retrieving metadata. 204 """ 205 return get(request, "project/project-id") 206 207 208def get_service_account_info(request, service_account="default"): 209 """Get information about a service account from the metadata server. 210 211 Args: 212 request (google.auth.transport.Request): A callable used to make 213 HTTP requests. 214 service_account (str): The string 'default' or a service account email 215 address. The determines which service account for which to acquire 216 information. 217 218 Returns: 219 Mapping: The service account's information, for example:: 220 221 { 222 'email': '...', 223 'scopes': ['scope', ...], 224 'aliases': ['default', '...'] 225 } 226 227 Raises: 228 google.auth.exceptions.TransportError: if an error occurred while 229 retrieving metadata. 230 """ 231 path = "instance/service-accounts/{0}/".format(service_account) 232 # See https://cloud.google.com/compute/docs/metadata#aggcontents 233 # for more on the use of 'recursive'. 234 return get(request, path, params={"recursive": "true"}) 235 236 237def get_service_account_token(request, service_account="default", scopes=None): 238 """Get the OAuth 2.0 access token for a service account. 239 240 Args: 241 request (google.auth.transport.Request): A callable used to make 242 HTTP requests. 243 service_account (str): The string 'default' or a service account email 244 address. The determines which service account for which to acquire 245 an access token. 246 scopes (Optional[Union[str, List[str]]]): Optional string or list of 247 strings with auth scopes. 248 Returns: 249 Union[str, datetime]: The access token and its expiration. 250 251 Raises: 252 google.auth.exceptions.TransportError: if an error occurred while 253 retrieving metadata. 254 """ 255 if scopes: 256 if not isinstance(scopes, str): 257 scopes = ",".join(scopes) 258 params = {"scopes": scopes} 259 else: 260 params = None 261 262 path = "instance/service-accounts/{0}/token".format(service_account) 263 token_json = get(request, path, params=params) 264 token_expiry = _helpers.utcnow() + datetime.timedelta( 265 seconds=token_json["expires_in"] 266 ) 267 return token_json["access_token"], token_expiry 268