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 http.client as http_client
22import json
23import logging
24import os
25from urllib.parse import urljoin
26
27from google.auth import _helpers
28from google.auth import environment_vars
29from google.auth import exceptions
30from google.auth import metrics
31from google.auth import transport
32from google.auth._exponential_backoff import ExponentialBackoff
33
34_LOGGER = logging.getLogger(__name__)
35
36# Environment variable GCE_METADATA_HOST is originally named
37# GCE_METADATA_ROOT. For compatibility reasons, here it checks
38# the new variable first; if not set, the system falls back
39# to the old variable.
40_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None)
41if not _GCE_METADATA_HOST:
42 _GCE_METADATA_HOST = os.getenv(
43 environment_vars.GCE_METADATA_ROOT, "metadata.google.internal"
44 )
45_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST)
46
47# This is used to ping the metadata server, it avoids the cost of a DNS
48# lookup.
49_METADATA_IP_ROOT = "http://{}".format(
50 os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254")
51)
52_METADATA_FLAVOR_HEADER = "metadata-flavor"
53_METADATA_FLAVOR_VALUE = "Google"
54_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
55
56# Timeout in seconds to wait for the GCE metadata server when detecting the
57# GCE environment.
58try:
59 _METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3))
60except ValueError: # pragma: NO COVER
61 _METADATA_DEFAULT_TIMEOUT = 3
62
63# Detect GCE Residency
64_GOOGLE = "Google"
65_GCE_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"
66
67
68def is_on_gce(request):
69 """Checks to see if the code runs on Google Compute Engine
70
71 Args:
72 request (google.auth.transport.Request): A callable used to make
73 HTTP requests.
74
75 Returns:
76 bool: True if the code runs on Google Compute Engine, False otherwise.
77 """
78 if ping(request):
79 return True
80
81 if os.name == "nt":
82 # TODO: implement GCE residency detection on Windows
83 return False
84
85 # Detect GCE residency on Linux
86 return detect_gce_residency_linux()
87
88
89def detect_gce_residency_linux():
90 """Detect Google Compute Engine residency by smbios check on Linux
91
92 Returns:
93 bool: True if the GCE product name file is detected, False otherwise.
94 """
95 try:
96 with open(_GCE_PRODUCT_NAME_FILE, "r") as file_obj:
97 content = file_obj.read().strip()
98
99 except Exception:
100 return False
101
102 return content.startswith(_GOOGLE)
103
104
105def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
106 """Checks to see if the metadata server is available.
107
108 Args:
109 request (google.auth.transport.Request): A callable used to make
110 HTTP requests.
111 timeout (int): How long to wait for the metadata server to respond.
112 retry_count (int): How many times to attempt connecting to metadata
113 server using above timeout.
114
115 Returns:
116 bool: True if the metadata server is reachable, False otherwise.
117 """
118 # NOTE: The explicit ``timeout`` is a workaround. The underlying
119 # issue is that resolving an unknown host on some networks will take
120 # 20-30 seconds; making this timeout short fixes the issue, but
121 # could lead to false negatives in the event that we are on GCE, but
122 # the metadata resolution was particularly slow. The latter case is
123 # "unlikely".
124 headers = _METADATA_HEADERS.copy()
125 headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping()
126
127 backoff = ExponentialBackoff(total_attempts=retry_count)
128
129 for attempt in backoff:
130 try:
131 response = request(
132 url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout
133 )
134
135 metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
136 return (
137 response.status == http_client.OK
138 and metadata_flavor == _METADATA_FLAVOR_VALUE
139 )
140
141 except exceptions.TransportError as e:
142 _LOGGER.warning(
143 "Compute Engine Metadata server unavailable on "
144 "attempt %s of %s. Reason: %s",
145 attempt,
146 retry_count,
147 e,
148 )
149
150 return False
151
152
153def get(
154 request,
155 path,
156 root=_METADATA_ROOT,
157 params=None,
158 recursive=False,
159 retry_count=5,
160 headers=None,
161 return_none_for_not_found_error=False,
162 timeout=_METADATA_DEFAULT_TIMEOUT,
163):
164 """Fetch a resource from the metadata server.
165
166 Args:
167 request (google.auth.transport.Request): A callable used to make
168 HTTP requests.
169 path (str): The resource to retrieve. For example,
170 ``'instance/service-accounts/default'``.
171 root (str): The full path to the metadata server root.
172 params (Optional[Mapping[str, str]]): A mapping of query parameter
173 keys to values.
174 recursive (bool): Whether to do a recursive query of metadata. See
175 https://cloud.google.com/compute/docs/metadata#aggcontents for more
176 details.
177 retry_count (int): How many times to attempt connecting to metadata
178 server using above timeout.
179 headers (Optional[Mapping[str, str]]): Headers for the request.
180 return_none_for_not_found_error (Optional[bool]): If True, returns None
181 for 404 error instead of throwing an exception.
182 timeout (int): How long to wait, in seconds for the metadata server to respond.
183
184 Returns:
185 Union[Mapping, str]: If the metadata server returns JSON, a mapping of
186 the decoded JSON is returned. Otherwise, the response content is
187 returned as a string.
188
189 Raises:
190 google.auth.exceptions.TransportError: if an error occurred while
191 retrieving metadata.
192 """
193 base_url = urljoin(root, path)
194 query_params = {} if params is None else params
195
196 headers_to_use = _METADATA_HEADERS.copy()
197 if headers:
198 headers_to_use.update(headers)
199
200 if recursive:
201 query_params["recursive"] = "true"
202
203 url = _helpers.update_query(base_url, query_params)
204
205 backoff = ExponentialBackoff(total_attempts=retry_count)
206 failure_reason = None
207 for attempt in backoff:
208 try:
209 response = request(
210 url=url, method="GET", headers=headers_to_use, timeout=timeout
211 )
212 if response.status in transport.DEFAULT_RETRYABLE_STATUS_CODES:
213 _LOGGER.warning(
214 "Compute Engine Metadata server unavailable on "
215 "attempt %s of %s. Response status: %s",
216 attempt,
217 retry_count,
218 response.status,
219 )
220 failure_reason = (
221 response.data.decode("utf-8")
222 if hasattr(response.data, "decode")
223 else response.data
224 )
225 continue
226 else:
227 break
228
229 except exceptions.TransportError as e:
230 _LOGGER.warning(
231 "Compute Engine Metadata server unavailable on "
232 "attempt %s of %s. Reason: %s",
233 attempt,
234 retry_count,
235 e,
236 )
237 failure_reason = e
238 else:
239 raise exceptions.TransportError(
240 "Failed to retrieve {} from the Google Compute Engine "
241 "metadata service. Compute Engine Metadata server unavailable due to {}".format(
242 url, failure_reason
243 )
244 )
245
246 content = _helpers.from_bytes(response.data)
247
248 if response.status == http_client.NOT_FOUND and return_none_for_not_found_error:
249 return None
250
251 if response.status == http_client.OK:
252 if (
253 _helpers.parse_content_type(response.headers["content-type"])
254 == "application/json"
255 ):
256 try:
257 return json.loads(content)
258 except ValueError as caught_exc:
259 new_exc = exceptions.TransportError(
260 "Received invalid JSON from the Google Compute Engine "
261 "metadata service: {:.20}".format(content)
262 )
263 raise new_exc from caught_exc
264 else:
265 return content
266
267 raise exceptions.TransportError(
268 "Failed to retrieve {} from the Google Compute Engine "
269 "metadata service. Status: {} Response:\n{}".format(
270 url, response.status, response.data
271 ),
272 response,
273 )
274
275
276def get_project_id(request):
277 """Get the Google Cloud Project ID from the metadata server.
278
279 Args:
280 request (google.auth.transport.Request): A callable used to make
281 HTTP requests.
282
283 Returns:
284 str: The project ID
285
286 Raises:
287 google.auth.exceptions.TransportError: if an error occurred while
288 retrieving metadata.
289 """
290 return get(request, "project/project-id")
291
292
293def get_universe_domain(request):
294 """Get the universe domain value from the metadata server.
295
296 Args:
297 request (google.auth.transport.Request): A callable used to make
298 HTTP requests.
299
300 Returns:
301 str: The universe domain value. If the universe domain endpoint is not
302 not found, return the default value, which is googleapis.com
303
304 Raises:
305 google.auth.exceptions.TransportError: if an error other than
306 404 occurs while retrieving metadata.
307 """
308 universe_domain = get(
309 request, "universe/universe-domain", return_none_for_not_found_error=True
310 )
311 if not universe_domain:
312 return "googleapis.com"
313 return universe_domain
314
315
316def get_service_account_info(request, service_account="default"):
317 """Get information about a service account from the metadata server.
318
319 Args:
320 request (google.auth.transport.Request): A callable used to make
321 HTTP requests.
322 service_account (str): The string 'default' or a service account email
323 address. The determines which service account for which to acquire
324 information.
325
326 Returns:
327 Mapping: The service account's information, for example::
328
329 {
330 'email': '...',
331 'scopes': ['scope', ...],
332 'aliases': ['default', '...']
333 }
334
335 Raises:
336 google.auth.exceptions.TransportError: if an error occurred while
337 retrieving metadata.
338 """
339 path = "instance/service-accounts/{0}/".format(service_account)
340 # See https://cloud.google.com/compute/docs/metadata#aggcontents
341 # for more on the use of 'recursive'.
342 return get(request, path, params={"recursive": "true"})
343
344
345def get_service_account_token(request, service_account="default", scopes=None):
346 """Get the OAuth 2.0 access token for a service account.
347
348 Args:
349 request (google.auth.transport.Request): A callable used to make
350 HTTP requests.
351 service_account (str): The string 'default' or a service account email
352 address. The determines which service account for which to acquire
353 an access token.
354 scopes (Optional[Union[str, List[str]]]): Optional string or list of
355 strings with auth scopes.
356 Returns:
357 Tuple[str, datetime]: The access token and its expiration.
358
359 Raises:
360 google.auth.exceptions.TransportError: if an error occurred while
361 retrieving metadata.
362 """
363 if scopes:
364 if not isinstance(scopes, str):
365 scopes = ",".join(scopes)
366 params = {"scopes": scopes}
367 else:
368 params = None
369
370 metrics_header = {
371 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
372 }
373
374 path = "instance/service-accounts/{0}/token".format(service_account)
375 token_json = get(request, path, params=params, headers=metrics_header)
376 token_expiry = _helpers.utcnow() + datetime.timedelta(
377 seconds=token_json["expires_in"]
378 )
379 return token_json["access_token"], token_expiry