codekingpro/portable-devtools
114k
1# Copyright 2016 Google LLC2#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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# 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 datetime21import http.client as http_client22import json23import logging24import os25from urllib.parse import urljoin26 27from google.auth import _helpers28from google.auth import environment_vars29from google.auth import exceptions30from google.auth import metrics31 32_LOGGER = logging.getLogger(__name__)33 34# Environment variable GCE_METADATA_HOST is originally named35# GCE_METADATA_ROOT. For compatiblity reasons, here it checks36# the new variable first; if not set, the system falls back37# 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)44 45# This is used to ping the metadata server, it avoids the cost of a DNS46# 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}53 54# Timeout in seconds to wait for the GCE metadata server when detecting the55# GCE environment.56try:57 _METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3))58except ValueError: # pragma: NO COVER59 _METADATA_DEFAULT_TIMEOUT = 360 61# Detect GCE Residency62_GOOGLE = "Google"63_GCE_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"64 65 66def is_on_gce(request):67 """Checks to see if the code runs on Google Compute Engine68 69 Args:70 request (google.auth.transport.Request): A callable used to make71 HTTP requests.72 73 Returns:74 bool: True if the code runs on Google Compute Engine, False otherwise.75 """76 if ping(request):77 return True78 79 if os.name == "nt":80 # TODO: implement GCE residency detection on Windows81 return False82 83 # Detect GCE residency on Linux84 return detect_gce_residency_linux()85 86 87def detect_gce_residency_linux():88 """Detect Google Compute Engine residency by smbios check on Linux89 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()96 97 except Exception:98 return False99 100 return content.startswith(_GOOGLE)101 102 103def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):104 """Checks to see if the metadata server is available.105 106 Args:107 request (google.auth.transport.Request): A callable used to make108 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 metadata111 server using above timeout.112 113 Returns:114 bool: True if the metadata server is reachable, False otherwise.115 """116 # NOTE: The explicit ``timeout`` is a workaround. The underlying117 # issue is that resolving an unknown host on some networks will take118 # 20-30 seconds; making this timeout short fixes the issue, but119 # could lead to false negatives in the event that we are on GCE, but120 # the metadata resolution was particularly slow. The latter case is121 # "unlikely".122 retries = 0123 headers = _METADATA_HEADERS.copy()124 headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping()125 126 while retries < retry_count:127 try:128 response = request(129 url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout130 )131 132 metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)133 return (134 response.status == http_client.OK135 and metadata_flavor == _METADATA_FLAVOR_VALUE136 )137 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 += 1147 148 return False149 150 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.162 163 Args:164 request (google.auth.transport.Request): A callable used to make165 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 parameter170 keys to values.171 recursive (bool): Whether to do a recursive query of metadata. See172 https://cloud.google.com/compute/docs/metadata#aggcontents for more173 details.174 retry_count (int): How many times to attempt connecting to metadata175 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 None178 for 404 error instead of throwing an exception.179 180 Returns:181 Union[Mapping, str]: If the metadata server returns JSON, a mapping of182 the decoded JSON is return. Otherwise, the response content is183 returned as a string.184 185 Raises:186 google.auth.exceptions.TransportError: if an error occurred while187 retrieving metadata.188 """189 base_url = urljoin(root, path)190 query_params = {} if params is None else params191 192 headers_to_use = _METADATA_HEADERS.copy()193 if headers:194 headers_to_use.update(headers)195 196 if recursive:197 query_params["recursive"] = "true"198 199 url = _helpers.update_query(base_url, query_params)200 201 retries = 0202 while retries < retry_count:203 try:204 response = request(url=url, method="GET", headers=headers_to_use)205 break206 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 += 1216 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 )221 222 content = _helpers.from_bytes(response.data)223 224 if response.status == http_client.NOT_FOUND and return_none_for_not_found_error:225 return None226 227 if response.status == http_client.OK:228 if (229 _helpers.parse_content_type(response.headers["content-type"])230 == "application/json"231 ):232 try:233 return json.loads(content)234 except ValueError as caught_exc:235 new_exc = exceptions.TransportError(236 "Received invalid JSON from the Google Compute Engine "237 "metadata service: {:.20}".format(content)238 )239 raise new_exc from caught_exc240 else:241 return content242 243 raise exceptions.TransportError(244 "Failed to retrieve {} from the Google Compute Engine "245 "metadata service. Status: {} Response:\n{}".format(246 url, response.status, response.data247 ),248 response,249 )250 251 252def get_project_id(request):253 """Get the Google Cloud Project ID from the metadata server.254 255 Args:256 request (google.auth.transport.Request): A callable used to make257 HTTP requests.258 259 Returns:260 str: The project ID261 262 Raises:263 google.auth.exceptions.TransportError: if an error occurred while264 retrieving metadata.265 """266 return get(request, "project/project-id")267 268 269def get_universe_domain(request):270 """Get the universe domain value from the metadata server.271 272 Args:273 request (google.auth.transport.Request): A callable used to make274 HTTP requests.275 276 Returns:277 str: The universe domain value. If the universe domain endpoint is not278 not found, return the default value, which is googleapis.com279 280 Raises:281 google.auth.exceptions.TransportError: if an error other than282 404 occurs while retrieving metadata.283 """284 universe_domain = get(285 request, "universe/universe_domain", return_none_for_not_found_error=True286 )287 if not universe_domain:288 return "googleapis.com"289 return universe_domain290 291 292def get_service_account_info(request, service_account="default"):293 """Get information about a service account from the metadata server.294 295 Args:296 request (google.auth.transport.Request): A callable used to make297 HTTP requests.298 service_account (str): The string 'default' or a service account email299 address. The determines which service account for which to acquire300 information.301 302 Returns:303 Mapping: The service account's information, for example::304 305 {306 'email': '...',307 'scopes': ['scope', ...],308 'aliases': ['default', '...']309 }310 311 Raises:312 google.auth.exceptions.TransportError: if an error occurred while313 retrieving metadata.314 """315 path = "instance/service-accounts/{0}/".format(service_account)316 # See https://cloud.google.com/compute/docs/metadata#aggcontents317 # for more on the use of 'recursive'.318 return get(request, path, params={"recursive": "true"})319 320 321def get_service_account_token(request, service_account="default", scopes=None):322 """Get the OAuth 2.0 access token for a service account.323 324 Args:325 request (google.auth.transport.Request): A callable used to make326 HTTP requests.327 service_account (str): The string 'default' or a service account email328 address. The determines which service account for which to acquire329 an access token.330 scopes (Optional[Union[str, List[str]]]): Optional string or list of331 strings with auth scopes.332 Returns:333 Tuple[str, datetime]: The access token and its expiration.334 335 Raises:336 google.auth.exceptions.TransportError: if an error occurred while337 retrieving metadata.338 """339 if scopes:340 if not isinstance(scopes, str):341 scopes = ",".join(scopes)342 params = {"scopes": scopes}343 else:344 params = None345 346 metrics_header = {347 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()348 }349 350 path = "instance/service-accounts/{0}/token".format(service_account)351 token_json = get(request, path, params=params, headers=metrics_header)352 token_expiry = _helpers.utcnow() + datetime.timedelta(353 seconds=token_json["expires_in"]354 )355 return token_json["access_token"], token_expiry356 