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):
163 """Fetch a resource from the metadata server.
164
165 Args:
166 request (google.auth.transport.Request): A callable used to make
167 HTTP requests.
168 path (str): The resource to retrieve. For example,
169 ``'instance/service-accounts/default'``.
170 root (str): The full path to the metadata server root.
171 params (Optional[Mapping[str, str]]): A mapping of query parameter
172 keys to values.
173 recursive (bool): Whether to do a recursive query of metadata. See
174 https://cloud.google.com/compute/docs/metadata#aggcontents for more
175 details.
176 retry_count (int): How many times to attempt connecting to metadata
177 server using above timeout.
178 headers (Optional[Mapping[str, str]]): Headers for the request.
179 return_none_for_not_found_error (Optional[bool]): If True, returns None
180 for 404 error instead of throwing an exception.
181
182 Returns:
183 Union[Mapping, str]: If the metadata server returns JSON, a mapping of
184 the decoded JSON is returned. Otherwise, the response content is
185 returned as a string.
186
187 Raises:
188 google.auth.exceptions.TransportError: if an error occurred while
189 retrieving metadata.
190 """
191 base_url = urljoin(root, path)
192 query_params = {} if params is None else params
193
194 headers_to_use = _METADATA_HEADERS.copy()
195 if headers:
196 headers_to_use.update(headers)
197
198 if recursive:
199 query_params["recursive"] = "true"
200
201 url = _helpers.update_query(base_url, query_params)
202
203 backoff = ExponentialBackoff(total_attempts=retry_count)
204
205 for attempt in backoff:
206 try:
207 response = request(url=url, method="GET", headers=headers_to_use)
208 if response.status in transport.DEFAULT_RETRYABLE_STATUS_CODES:
209 _LOGGER.warning(
210 "Compute Engine Metadata server unavailable on "
211 "attempt %s of %s. Response status: %s",
212 attempt,
213 retry_count,
214 response.status,
215 )
216 continue
217 else:
218 break
219
220 except exceptions.TransportError as e:
221 _LOGGER.warning(
222 "Compute Engine Metadata server unavailable on "
223 "attempt %s of %s. Reason: %s",
224 attempt,
225 retry_count,
226 e,
227 )
228 else:
229 raise exceptions.TransportError(
230 "Failed to retrieve {} from the Google Compute Engine "
231 "metadata service. Compute Engine Metadata server unavailable".format(url)
232 )
233
234 content = _helpers.from_bytes(response.data)
235
236 if response.status == http_client.NOT_FOUND and return_none_for_not_found_error:
237 return None
238
239 if response.status == http_client.OK:
240 if (
241 _helpers.parse_content_type(response.headers["content-type"])
242 == "application/json"
243 ):
244 try:
245 return json.loads(content)
246 except ValueError as caught_exc:
247 new_exc = exceptions.TransportError(
248 "Received invalid JSON from the Google Compute Engine "
249 "metadata service: {:.20}".format(content)
250 )
251 raise new_exc from caught_exc
252 else:
253 return content
254
255 raise exceptions.TransportError(
256 "Failed to retrieve {} from the Google Compute Engine "
257 "metadata service. Status: {} Response:\n{}".format(
258 url, response.status, response.data
259 ),
260 response,
261 )
262
263
264def get_project_id(request):
265 """Get the Google Cloud Project ID from the metadata server.
266
267 Args:
268 request (google.auth.transport.Request): A callable used to make
269 HTTP requests.
270
271 Returns:
272 str: The project ID
273
274 Raises:
275 google.auth.exceptions.TransportError: if an error occurred while
276 retrieving metadata.
277 """
278 return get(request, "project/project-id")
279
280
281def get_universe_domain(request):
282 """Get the universe domain value from the metadata server.
283
284 Args:
285 request (google.auth.transport.Request): A callable used to make
286 HTTP requests.
287
288 Returns:
289 str: The universe domain value. If the universe domain endpoint is not
290 not found, return the default value, which is googleapis.com
291
292 Raises:
293 google.auth.exceptions.TransportError: if an error other than
294 404 occurs while retrieving metadata.
295 """
296 universe_domain = get(
297 request, "universe/universe_domain", return_none_for_not_found_error=True
298 )
299 if not universe_domain:
300 return "googleapis.com"
301 return universe_domain
302
303
304def get_service_account_info(request, service_account="default"):
305 """Get information about a service account from the metadata server.
306
307 Args:
308 request (google.auth.transport.Request): A callable used to make
309 HTTP requests.
310 service_account (str): The string 'default' or a service account email
311 address. The determines which service account for which to acquire
312 information.
313
314 Returns:
315 Mapping: The service account's information, for example::
316
317 {
318 'email': '...',
319 'scopes': ['scope', ...],
320 'aliases': ['default', '...']
321 }
322
323 Raises:
324 google.auth.exceptions.TransportError: if an error occurred while
325 retrieving metadata.
326 """
327 path = "instance/service-accounts/{0}/".format(service_account)
328 # See https://cloud.google.com/compute/docs/metadata#aggcontents
329 # for more on the use of 'recursive'.
330 return get(request, path, params={"recursive": "true"})
331
332
333def get_service_account_token(request, service_account="default", scopes=None):
334 """Get the OAuth 2.0 access token for a service account.
335
336 Args:
337 request (google.auth.transport.Request): A callable used to make
338 HTTP requests.
339 service_account (str): The string 'default' or a service account email
340 address. The determines which service account for which to acquire
341 an access token.
342 scopes (Optional[Union[str, List[str]]]): Optional string or list of
343 strings with auth scopes.
344 Returns:
345 Tuple[str, datetime]: The access token and its expiration.
346
347 Raises:
348 google.auth.exceptions.TransportError: if an error occurred while
349 retrieving metadata.
350 """
351 if scopes:
352 if not isinstance(scopes, str):
353 scopes = ",".join(scopes)
354 params = {"scopes": scopes}
355 else:
356 params = None
357
358 metrics_header = {
359 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
360 }
361
362 path = "instance/service-accounts/{0}/token".format(service_account)
363 token_json = get(request, path, params=params, headers=metrics_header)
364 token_expiry = _helpers.utcnow() + datetime.timedelta(
365 seconds=token_json["expires_in"]
366 )
367 return token_json["access_token"], token_expiry