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