12d57dc69SVladimir Kotal# 22d57dc69SVladimir Kotal# CDDL HEADER START 32d57dc69SVladimir Kotal# 42d57dc69SVladimir Kotal# The contents of this file are subject to the terms of the 52d57dc69SVladimir Kotal# Common Development and Distribution License (the "License"). 62d57dc69SVladimir Kotal# You may not use this file except in compliance with the License. 72d57dc69SVladimir Kotal# 82d57dc69SVladimir Kotal# See LICENSE.txt included in this distribution for the specific 92d57dc69SVladimir Kotal# language governing permissions and limitations under the License. 102d57dc69SVladimir Kotal# 112d57dc69SVladimir Kotal# When distributing Covered Code, include this CDDL HEADER in each 122d57dc69SVladimir Kotal# file and include the License file at LICENSE.txt. 132d57dc69SVladimir Kotal# If applicable, add the following below this CDDL HEADER, with the 142d57dc69SVladimir Kotal# fields enclosed by brackets "[]" replaced with your own identifying 152d57dc69SVladimir Kotal# information: Portions Copyright [yyyy] [name of copyright owner] 162d57dc69SVladimir Kotal# 172d57dc69SVladimir Kotal# CDDL HEADER END 182d57dc69SVladimir Kotal# 192d57dc69SVladimir Kotal 202d57dc69SVladimir Kotal# 21cc7022c6SVladimir Kotal# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. 222d57dc69SVladimir Kotal# 232d57dc69SVladimir Kotal 242d57dc69SVladimir Kotalimport json 252d57dc69SVladimir Kotalimport logging 261c258122SVladimir Kotalimport time 272d57dc69SVladimir Kotal 282d57dc69SVladimir Kotalimport requests 292d57dc69SVladimir Kotal 30*c41895f8SVladimir Kotalfrom .webutil import get_proxies, is_web_uri 312d57dc69SVladimir Kotal 322d57dc69SVladimir KotalCONTENT_TYPE = 'Content-Type' 332d57dc69SVladimir KotalAPPLICATION_JSON = 'application/json' # default 342d57dc69SVladimir Kotal 352d57dc69SVladimir Kotal 36959b34e9SVladimir Kotaldef call_finished(location_uri, headers, timeout): 37959b34e9SVladimir Kotal """ 38959b34e9SVladimir Kotal :param location_uri: URI to check the status of API call 39959b34e9SVladimir Kotal :param headers: HTTP headers 40959b34e9SVladimir Kotal :param timeout: connect timeout 415cce3163SVladimir Kotal :return indication and response tuple 42959b34e9SVladimir Kotal """ 43959b34e9SVladimir Kotal logger = logging.getLogger(__name__) 44959b34e9SVladimir Kotal 45959b34e9SVladimir Kotal logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}") 46959b34e9SVladimir Kotal response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 47959b34e9SVladimir Kotal if response is None: 48959b34e9SVladimir Kotal raise Exception("API call failed") 49959b34e9SVladimir Kotal 50959b34e9SVladimir Kotal response.raise_for_status() 51959b34e9SVladimir Kotal if response.status_code == 202: 525cce3163SVladimir Kotal return False, response 53959b34e9SVladimir Kotal else: 545cce3163SVladimir Kotal return True, response 55959b34e9SVladimir Kotal 56959b34e9SVladimir Kotal 57959b34e9SVladimir Kotaldef wait_for_async_api(response, api_timeout=None, headers=None, timeout=None): 581c258122SVladimir Kotal """ 591c258122SVladimir Kotal :param response: request 60959b34e9SVladimir Kotal :param api_timeout: asynchronous API timeout (will wait forever or until error if None) 611c258122SVladimir Kotal :param headers: request headers 621c258122SVladimir Kotal :param timeout: connect timeout 631c258122SVladimir Kotal :return: request 641c258122SVladimir Kotal """ 651c258122SVladimir Kotal logger = logging.getLogger(__name__) 661c258122SVladimir Kotal 671c258122SVladimir Kotal location_uri = response.headers.get("Location") 681c258122SVladimir Kotal if location_uri is None: 691c258122SVladimir Kotal raise Exception(f"no Location header in {response}") 701c258122SVladimir Kotal 71cc7022c6SVladimir Kotal start_time = time.time() 72959b34e9SVladimir Kotal if api_timeout is None: 73959b34e9SVladimir Kotal while True: 745cce3163SVladimir Kotal done, response = call_finished(location_uri, headers, timeout) 755cce3163SVladimir Kotal if done: 76959b34e9SVladimir Kotal break 771c258122SVladimir Kotal time.sleep(1) 781c258122SVladimir Kotal else: 79959b34e9SVladimir Kotal for _ in range(api_timeout): 805cce3163SVladimir Kotal done, response = call_finished(location_uri, headers, timeout) 815cce3163SVladimir Kotal if done: 821c258122SVladimir Kotal break 83959b34e9SVladimir Kotal time.sleep(1) 841c258122SVladimir Kotal 851c258122SVladimir Kotal if response.status_code == 202: 86cc7022c6SVladimir Kotal wait_time = time.time() - start_time 87cc7022c6SVladimir Kotal logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}") 881c258122SVladimir Kotal return response 891c258122SVladimir Kotal 901c258122SVladimir Kotal logger.debug(f"DELETE API call to {location_uri}") 911c258122SVladimir Kotal requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 921c258122SVladimir Kotal 931c258122SVladimir Kotal return response 941c258122SVladimir Kotal 951c258122SVladimir Kotal 96*c41895f8SVladimir Kotaldef do_api_call(method, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None): 972d57dc69SVladimir Kotal """ 982d57dc69SVladimir Kotal Perform an API call. Will raise an exception if the request fails. 99*c41895f8SVladimir Kotal :param method: string holding HTTP verb 1002d57dc69SVladimir Kotal :param uri: URI string 1012d57dc69SVladimir Kotal :param params: request parameters 1022d57dc69SVladimir Kotal :param headers: HTTP headers dictionary 1032d57dc69SVladimir Kotal :param data: data or None 104732be1c2SVladimir Kotal :param timeout: optional connect timeout in seconds. 1051c258122SVladimir Kotal Applies also to asynchronous API status calls. 1061c258122SVladimir Kotal :param api_timeout: optional timeout for asynchronous API requests in seconds. 1072d57dc69SVladimir Kotal :return: the result of the handler call, can be None 1082d57dc69SVladimir Kotal """ 1092d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 1102d57dc69SVladimir Kotal 111*c41895f8SVladimir Kotal handler = getattr(requests, method.lower()) 1122d57dc69SVladimir Kotal if handler is None or not callable(handler): 113*c41895f8SVladimir Kotal raise Exception('Unknown HTTP method: {}'.format(method)) 1142d57dc69SVladimir Kotal 115cc7022c6SVladimir Kotal logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}". 116*c41895f8SVladimir Kotal format(method, uri, data, timeout, api_timeout, headers)) 1172d57dc69SVladimir Kotal r = handler( 1182d57dc69SVladimir Kotal uri, 1192d57dc69SVladimir Kotal data=data, 1202d57dc69SVladimir Kotal params=params, 1212d57dc69SVladimir Kotal headers=headers, 122732be1c2SVladimir Kotal proxies=get_proxies(uri), 123732be1c2SVladimir Kotal timeout=timeout 1242d57dc69SVladimir Kotal ) 1252d57dc69SVladimir Kotal 1262d57dc69SVladimir Kotal if r is None: 1272d57dc69SVladimir Kotal raise Exception("API call failed") 1282d57dc69SVladimir Kotal 1291c258122SVladimir Kotal if r.status_code == 202: 130959b34e9SVladimir Kotal r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout) 1311c258122SVladimir Kotal 1322d57dc69SVladimir Kotal r.raise_for_status() 1332d57dc69SVladimir Kotal 1342d57dc69SVladimir Kotal return r 1352d57dc69SVladimir Kotal 1362d57dc69SVladimir Kotal 1372d97c0a2SVladimir Kotaldef subst(src, substitutions): 1382d97c0a2SVladimir Kotal if substitutions: 1392d97c0a2SVladimir Kotal for pattern, value in substitutions.items(): 1402d97c0a2SVladimir Kotal if value: 1412d97c0a2SVladimir Kotal src = src.replace(pattern, value) 1422d97c0a2SVladimir Kotal 1432d97c0a2SVladimir Kotal return src 1442d97c0a2SVladimir Kotal 1452d97c0a2SVladimir Kotal 146*c41895f8SVladimir Kotaldef get_call_props(call): 1472d57dc69SVladimir Kotal """ 148*c41895f8SVladimir Kotal Retrieve the basic properties of a call. 149*c41895f8SVladimir Kotal :param call: dictionary 150*c41895f8SVladimir Kotal :return: URI, HTTP method, data, headers 151*c41895f8SVladimir Kotal """ 152*c41895f8SVladimir Kotal 153*c41895f8SVladimir Kotal logger = logging.getLogger(__name__) 154*c41895f8SVladimir Kotal 155*c41895f8SVladimir Kotal uri = call.get("uri") 156*c41895f8SVladimir Kotal if not uri: 157*c41895f8SVladimir Kotal raise Exception(f"no 'uri' key present in {call}") 158*c41895f8SVladimir Kotal if not is_web_uri(uri): 159*c41895f8SVladimir Kotal raise Exception(f"not a valid URI: {uri}") 160*c41895f8SVladimir Kotal 161*c41895f8SVladimir Kotal method = call.get("method") 162*c41895f8SVladimir Kotal if not method: 163*c41895f8SVladimir Kotal logger.debug(f"no 'method' key in {call}, using GET") 164*c41895f8SVladimir Kotal method = "GET" 165*c41895f8SVladimir Kotal 166*c41895f8SVladimir Kotal data = call.get("data") 167*c41895f8SVladimir Kotal 168*c41895f8SVladimir Kotal try: 169*c41895f8SVladimir Kotal headers = call.get("headers") 170*c41895f8SVladimir Kotal if headers and not isinstance(headers, dict): 171*c41895f8SVladimir Kotal raise Exception("headers must be a dictionary") 172*c41895f8SVladimir Kotal except IndexError: 173*c41895f8SVladimir Kotal headers = {} 174*c41895f8SVladimir Kotal 175*c41895f8SVladimir Kotal if headers is None: 176*c41895f8SVladimir Kotal headers = {} 177*c41895f8SVladimir Kotal 178*c41895f8SVladimir Kotal return uri, method, data, headers 179*c41895f8SVladimir Kotal 180*c41895f8SVladimir Kotal 181*c41895f8SVladimir Kotaldef call_rest_api(call, substitutions=None, http_headers=None, timeout=None, api_timeout=None): 182*c41895f8SVladimir Kotal """ 183*c41895f8SVladimir Kotal Make REST API call. Occurrence of the pattern in the URI 1842d57dc69SVladimir Kotal (first part of the command) or data payload will be replaced by the name. 1852d57dc69SVladimir Kotal 1862d57dc69SVladimir Kotal Default content type is application/json. 1872d57dc69SVladimir Kotal 188*c41895f8SVladimir Kotal :param call: dictionary describing the properties of the API call 1892d97c0a2SVladimir Kotal :param substitutions: dictionary of pattern:value for command and/or 1902d97c0a2SVladimir Kotal data substitution 19189229afdSVladimir Kotal :param http_headers: optional dictionary of HTTP headers to be appended 192732be1c2SVladimir Kotal :param timeout: optional timeout in seconds for API call response 19316e733aaSVladimir Kotal :param api_timeout: optional timeout in seconds for asynchronous API call 194959b34e9SVladimir Kotal :return value from given requests method 1952d57dc69SVladimir Kotal """ 1962d57dc69SVladimir Kotal 1972d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 1982d57dc69SVladimir Kotal 199*c41895f8SVladimir Kotal uri, verb, data, headers = get_call_props(call) 2002d57dc69SVladimir Kotal 201*c41895f8SVladimir Kotal logger.debug(f"Headers from the call structure: {headers}") 20289229afdSVladimir Kotal if http_headers: 203*c41895f8SVladimir Kotal logger.debug("Updating HTTP headers for call {} with {}". 204*c41895f8SVladimir Kotal format(call, http_headers)) 20589229afdSVladimir Kotal headers.update(http_headers) 20689229afdSVladimir Kotal 207*c41895f8SVladimir Kotal logger.debug("Performing URI substitutions") 2082d97c0a2SVladimir Kotal uri = subst(uri, substitutions) 209*c41895f8SVladimir Kotal logger.debug(f"URI after the substitutions: {uri}") 210*c41895f8SVladimir Kotal 211*c41895f8SVladimir Kotal call_api_timeout = call.get("api_timeout") 212*c41895f8SVladimir Kotal if call_api_timeout: 213*c41895f8SVladimir Kotal logger.debug(f"Setting API timeout based on the call to {call_api_timeout}") 214*c41895f8SVladimir Kotal api_timeout = call_api_timeout 2152d57dc69SVladimir Kotal 2162d57dc69SVladimir Kotal if data: 217*c41895f8SVladimir Kotal header_names = [x.lower() for x in headers.keys()] 2182d57dc69SVladimir Kotal if CONTENT_TYPE.lower() not in header_names: 219*c41895f8SVladimir Kotal logger.debug("Adding HTTP header: {} = {}". 2202d57dc69SVladimir Kotal format(CONTENT_TYPE, APPLICATION_JSON)) 2212d57dc69SVladimir Kotal headers[CONTENT_TYPE] = APPLICATION_JSON 2222d57dc69SVladimir Kotal 2232d57dc69SVladimir Kotal for (k, v) in headers.items(): 2242d57dc69SVladimir Kotal if k.lower() == CONTENT_TYPE.lower(): 2252d57dc69SVladimir Kotal if headers[k].lower() == APPLICATION_JSON.lower(): 2262d57dc69SVladimir Kotal logger.debug("Converting {} to JSON".format(data)) 2272d57dc69SVladimir Kotal data = json.dumps(data) 2282d57dc69SVladimir Kotal break 2292d57dc69SVladimir Kotal 2302d97c0a2SVladimir Kotal data = subst(data, substitutions) 2312d57dc69SVladimir Kotal logger.debug("entity data: {}".format(data)) 2322d57dc69SVladimir Kotal 23316e733aaSVladimir Kotal return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout, api_timeout=api_timeout) 234