Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/auth/compute_engine/_metadata.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

104 statements  

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