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

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

108 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 timeout=_METADATA_DEFAULT_TIMEOUT, 

163): 

164 """Fetch a resource from the metadata server. 

165 

166 Args: 

167 request (google.auth.transport.Request): A callable used to make 

168 HTTP requests. 

169 path (str): The resource to retrieve. For example, 

170 ``'instance/service-accounts/default'``. 

171 root (str): The full path to the metadata server root. 

172 params (Optional[Mapping[str, str]]): A mapping of query parameter 

173 keys to values. 

174 recursive (bool): Whether to do a recursive query of metadata. See 

175 https://cloud.google.com/compute/docs/metadata#aggcontents for more 

176 details. 

177 retry_count (int): How many times to attempt connecting to metadata 

178 server using above timeout. 

179 headers (Optional[Mapping[str, str]]): Headers for the request. 

180 return_none_for_not_found_error (Optional[bool]): If True, returns None 

181 for 404 error instead of throwing an exception. 

182 timeout (int): How long to wait, in seconds for the metadata server to respond. 

183 

184 Returns: 

185 Union[Mapping, str]: If the metadata server returns JSON, a mapping of 

186 the decoded JSON is returned. Otherwise, the response content is 

187 returned as a string. 

188 

189 Raises: 

190 google.auth.exceptions.TransportError: if an error occurred while 

191 retrieving metadata. 

192 """ 

193 base_url = urljoin(root, path) 

194 query_params = {} if params is None else params 

195 

196 headers_to_use = _METADATA_HEADERS.copy() 

197 if headers: 

198 headers_to_use.update(headers) 

199 

200 if recursive: 

201 query_params["recursive"] = "true" 

202 

203 url = _helpers.update_query(base_url, query_params) 

204 

205 backoff = ExponentialBackoff(total_attempts=retry_count) 

206 failure_reason = None 

207 for attempt in backoff: 

208 try: 

209 response = request( 

210 url=url, method="GET", headers=headers_to_use, timeout=timeout 

211 ) 

212 if response.status in transport.DEFAULT_RETRYABLE_STATUS_CODES: 

213 _LOGGER.warning( 

214 "Compute Engine Metadata server unavailable on " 

215 "attempt %s of %s. Response status: %s", 

216 attempt, 

217 retry_count, 

218 response.status, 

219 ) 

220 failure_reason = ( 

221 response.data.decode("utf-8") 

222 if hasattr(response.data, "decode") 

223 else response.data 

224 ) 

225 continue 

226 else: 

227 break 

228 

229 except exceptions.TransportError as e: 

230 _LOGGER.warning( 

231 "Compute Engine Metadata server unavailable on " 

232 "attempt %s of %s. Reason: %s", 

233 attempt, 

234 retry_count, 

235 e, 

236 ) 

237 failure_reason = e 

238 else: 

239 raise exceptions.TransportError( 

240 "Failed to retrieve {} from the Google Compute Engine " 

241 "metadata service. Compute Engine Metadata server unavailable due to {}".format( 

242 url, failure_reason 

243 ) 

244 ) 

245 

246 content = _helpers.from_bytes(response.data) 

247 

248 if response.status == http_client.NOT_FOUND and return_none_for_not_found_error: 

249 return None 

250 

251 if response.status == http_client.OK: 

252 if ( 

253 _helpers.parse_content_type(response.headers["content-type"]) 

254 == "application/json" 

255 ): 

256 try: 

257 return json.loads(content) 

258 except ValueError as caught_exc: 

259 new_exc = exceptions.TransportError( 

260 "Received invalid JSON from the Google Compute Engine " 

261 "metadata service: {:.20}".format(content) 

262 ) 

263 raise new_exc from caught_exc 

264 else: 

265 return content 

266 

267 raise exceptions.TransportError( 

268 "Failed to retrieve {} from the Google Compute Engine " 

269 "metadata service. Status: {} Response:\n{}".format( 

270 url, response.status, response.data 

271 ), 

272 response, 

273 ) 

274 

275 

276def get_project_id(request): 

277 """Get the Google Cloud Project ID from the metadata server. 

278 

279 Args: 

280 request (google.auth.transport.Request): A callable used to make 

281 HTTP requests. 

282 

283 Returns: 

284 str: The project ID 

285 

286 Raises: 

287 google.auth.exceptions.TransportError: if an error occurred while 

288 retrieving metadata. 

289 """ 

290 return get(request, "project/project-id") 

291 

292 

293def get_universe_domain(request): 

294 """Get the universe domain value from the metadata server. 

295 

296 Args: 

297 request (google.auth.transport.Request): A callable used to make 

298 HTTP requests. 

299 

300 Returns: 

301 str: The universe domain value. If the universe domain endpoint is not 

302 not found, return the default value, which is googleapis.com 

303 

304 Raises: 

305 google.auth.exceptions.TransportError: if an error other than 

306 404 occurs while retrieving metadata. 

307 """ 

308 universe_domain = get( 

309 request, "universe/universe-domain", return_none_for_not_found_error=True 

310 ) 

311 if not universe_domain: 

312 return "googleapis.com" 

313 return universe_domain 

314 

315 

316def get_service_account_info(request, service_account="default"): 

317 """Get information about a service account from the metadata server. 

318 

319 Args: 

320 request (google.auth.transport.Request): A callable used to make 

321 HTTP requests. 

322 service_account (str): The string 'default' or a service account email 

323 address. The determines which service account for which to acquire 

324 information. 

325 

326 Returns: 

327 Mapping: The service account's information, for example:: 

328 

329 { 

330 'email': '...', 

331 'scopes': ['scope', ...], 

332 'aliases': ['default', '...'] 

333 } 

334 

335 Raises: 

336 google.auth.exceptions.TransportError: if an error occurred while 

337 retrieving metadata. 

338 """ 

339 path = "instance/service-accounts/{0}/".format(service_account) 

340 # See https://cloud.google.com/compute/docs/metadata#aggcontents 

341 # for more on the use of 'recursive'. 

342 return get(request, path, params={"recursive": "true"}) 

343 

344 

345def get_service_account_token(request, service_account="default", scopes=None): 

346 """Get the OAuth 2.0 access token for a service account. 

347 

348 Args: 

349 request (google.auth.transport.Request): A callable used to make 

350 HTTP requests. 

351 service_account (str): The string 'default' or a service account email 

352 address. The determines which service account for which to acquire 

353 an access token. 

354 scopes (Optional[Union[str, List[str]]]): Optional string or list of 

355 strings with auth scopes. 

356 Returns: 

357 Tuple[str, datetime]: The access token and its expiration. 

358 

359 Raises: 

360 google.auth.exceptions.TransportError: if an error occurred while 

361 retrieving metadata. 

362 """ 

363 if scopes: 

364 if not isinstance(scopes, str): 

365 scopes = ",".join(scopes) 

366 params = {"scopes": scopes} 

367 else: 

368 params = None 

369 

370 metrics_header = { 

371 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds() 

372 } 

373 

374 path = "instance/service-accounts/{0}/token".format(service_account) 

375 token_json = get(request, path, params=params, headers=metrics_header) 

376 token_expiry = _helpers.utcnow() + datetime.timedelta( 

377 seconds=token_json["expires_in"] 

378 ) 

379 return token_json["access_token"], token_expiry