_metadata.py 9.2 KB

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