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

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 

31 

32_LOGGER = logging.getLogger(__name__) 

33 

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) 

44 

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} 

53 

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 

60 

61# Detect GCE Residency 

62_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 Engine 

68 

69 Args: 

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

71 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 True 

78 

79 if os.name == "nt": 

80 # TODO: implement GCE residency detection on Windows 

81 return False 

82 

83 # Detect GCE residency on Linux 

84 return detect_gce_residency_linux() 

85 

86 

87def detect_gce_residency_linux(): 

88 """Detect Google Compute Engine residency by smbios check on Linux 

89 

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 False 

99 

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 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. 

112 

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() 

125 

126 while retries < retry_count: 

127 try: 

128 response = request( 

129 url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout 

130 ) 

131 

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 ) 

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 += 1 

147 

148 return False 

149 

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 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. 

179 

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. 

184 

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 

191 

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 = 0 

202 while retries < retry_count: 

203 try: 

204 response = request(url=url, method="GET", headers=headers_to_use) 

205 break 

206 

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 ) 

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 _LOGGER.info( 

226 "Compute Engine Metadata server call to %s returned 404, reason: %s", 

227 path, 

228 content, 

229 ) 

230 return None 

231 

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 

247 

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 ) 

255 

256 

257def get_project_id(request): 

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

259 

260 Args: 

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

262 HTTP requests. 

263 

264 Returns: 

265 str: The project ID 

266 

267 Raises: 

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

269 retrieving metadata. 

270 """ 

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

272 

273 

274def get_universe_domain(request): 

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

276 

277 Args: 

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

279 HTTP requests. 

280 

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 

284 

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 

295 

296 

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

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

299 

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. 

306 

307 Returns: 

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

309 

310 { 

311 'email': '...', 

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

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

314 } 

315 

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"}) 

324 

325 

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

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

328 

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. 

339 

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 

350 

351 metrics_header = { 

352 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds() 

353 } 

354 

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