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 302d57dc69SVladimir Kotalfrom .patterns import COMMAND_PROPERTY 312d57dc69SVladimir Kotalfrom .webutil import get_proxies 322d57dc69SVladimir Kotal 332d57dc69SVladimir KotalCONTENT_TYPE = 'Content-Type' 342d57dc69SVladimir KotalAPPLICATION_JSON = 'application/json' # default 352d57dc69SVladimir Kotal 362d57dc69SVladimir Kotal 37959b34e9SVladimir Kotaldef call_finished(location_uri, headers, timeout): 38959b34e9SVladimir Kotal """ 39959b34e9SVladimir Kotal :param location_uri: URI to check the status of API call 40959b34e9SVladimir Kotal :param headers: HTTP headers 41959b34e9SVladimir Kotal :param timeout: connect timeout 42*5cce3163SVladimir Kotal :return indication and response tuple 43959b34e9SVladimir Kotal """ 44959b34e9SVladimir Kotal logger = logging.getLogger(__name__) 45959b34e9SVladimir Kotal 46959b34e9SVladimir Kotal logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}") 47959b34e9SVladimir Kotal response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 48959b34e9SVladimir Kotal if response is None: 49959b34e9SVladimir Kotal raise Exception("API call failed") 50959b34e9SVladimir Kotal 51959b34e9SVladimir Kotal response.raise_for_status() 52959b34e9SVladimir Kotal if response.status_code == 202: 53*5cce3163SVladimir Kotal return False, response 54959b34e9SVladimir Kotal else: 55*5cce3163SVladimir Kotal return True, response 56959b34e9SVladimir Kotal 57959b34e9SVladimir Kotal 58959b34e9SVladimir Kotaldef wait_for_async_api(response, api_timeout=None, headers=None, timeout=None): 591c258122SVladimir Kotal """ 601c258122SVladimir Kotal :param response: request 61959b34e9SVladimir Kotal :param api_timeout: asynchronous API timeout (will wait forever or until error if None) 621c258122SVladimir Kotal :param headers: request headers 631c258122SVladimir Kotal :param timeout: connect timeout 641c258122SVladimir Kotal :return: request 651c258122SVladimir Kotal """ 661c258122SVladimir Kotal logger = logging.getLogger(__name__) 671c258122SVladimir Kotal 681c258122SVladimir Kotal location_uri = response.headers.get("Location") 691c258122SVladimir Kotal if location_uri is None: 701c258122SVladimir Kotal raise Exception(f"no Location header in {response}") 711c258122SVladimir Kotal 72cc7022c6SVladimir Kotal start_time = time.time() 73959b34e9SVladimir Kotal if api_timeout is None: 74959b34e9SVladimir Kotal while True: 75*5cce3163SVladimir Kotal done, response = call_finished(location_uri, headers, timeout) 76*5cce3163SVladimir Kotal if done: 77959b34e9SVladimir Kotal break 781c258122SVladimir Kotal time.sleep(1) 791c258122SVladimir Kotal else: 80959b34e9SVladimir Kotal for _ in range(api_timeout): 81*5cce3163SVladimir Kotal done, response = call_finished(location_uri, headers, timeout) 82*5cce3163SVladimir Kotal if done: 831c258122SVladimir Kotal break 84959b34e9SVladimir Kotal time.sleep(1) 851c258122SVladimir Kotal 861c258122SVladimir Kotal if response.status_code == 202: 87cc7022c6SVladimir Kotal wait_time = time.time() - start_time 88cc7022c6SVladimir Kotal logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}") 891c258122SVladimir Kotal return response 901c258122SVladimir Kotal 911c258122SVladimir Kotal logger.debug(f"DELETE API call to {location_uri}") 921c258122SVladimir Kotal requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 931c258122SVladimir Kotal 941c258122SVladimir Kotal return response 951c258122SVladimir Kotal 961c258122SVladimir Kotal 97f8f28195SVladimir Kotaldef do_api_call(verb, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None): 982d57dc69SVladimir Kotal """ 992d57dc69SVladimir Kotal Perform an API call. Will raise an exception if the request fails. 1002d57dc69SVladimir Kotal :param verb: string holding HTTP verb 1012d57dc69SVladimir Kotal :param uri: URI string 1022d57dc69SVladimir Kotal :param params: request parameters 1032d57dc69SVladimir Kotal :param headers: HTTP headers dictionary 1042d57dc69SVladimir Kotal :param data: data or None 105732be1c2SVladimir Kotal :param timeout: optional connect timeout in seconds. 1061c258122SVladimir Kotal Applies also to asynchronous API status calls. 1071c258122SVladimir Kotal :param api_timeout: optional timeout for asynchronous API requests in seconds. 1082d57dc69SVladimir Kotal :return: the result of the handler call, can be None 1092d57dc69SVladimir Kotal """ 1102d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 1112d57dc69SVladimir Kotal 1122d57dc69SVladimir Kotal handler = getattr(requests, verb.lower()) 1132d57dc69SVladimir Kotal if handler is None or not callable(handler): 1142d57dc69SVladimir Kotal raise Exception('Unknown HTTP verb: {}'.format(verb)) 1152d57dc69SVladimir Kotal 116cc7022c6SVladimir Kotal logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}". 117cc7022c6SVladimir Kotal format(verb, uri, data, timeout, api_timeout, headers)) 1182d57dc69SVladimir Kotal r = handler( 1192d57dc69SVladimir Kotal uri, 1202d57dc69SVladimir Kotal data=data, 1212d57dc69SVladimir Kotal params=params, 1222d57dc69SVladimir Kotal headers=headers, 123732be1c2SVladimir Kotal proxies=get_proxies(uri), 124732be1c2SVladimir Kotal timeout=timeout 1252d57dc69SVladimir Kotal ) 1262d57dc69SVladimir Kotal 1272d57dc69SVladimir Kotal if r is None: 1282d57dc69SVladimir Kotal raise Exception("API call failed") 1292d57dc69SVladimir Kotal 1301c258122SVladimir Kotal if r.status_code == 202: 131959b34e9SVladimir Kotal r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout) 1321c258122SVladimir Kotal 1332d57dc69SVladimir Kotal r.raise_for_status() 1342d57dc69SVladimir Kotal 1352d57dc69SVladimir Kotal return r 1362d57dc69SVladimir Kotal 1372d57dc69SVladimir Kotal 1382d97c0a2SVladimir Kotaldef subst(src, substitutions): 1392d97c0a2SVladimir Kotal if substitutions: 1402d97c0a2SVladimir Kotal for pattern, value in substitutions.items(): 1412d97c0a2SVladimir Kotal if value: 1422d97c0a2SVladimir Kotal src = src.replace(pattern, value) 1432d97c0a2SVladimir Kotal 1442d97c0a2SVladimir Kotal return src 1452d97c0a2SVladimir Kotal 1462d97c0a2SVladimir Kotal 14716e733aaSVladimir Kotaldef call_rest_api(command, substitutions=None, http_headers=None, timeout=None, api_timeout=None): 1482d57dc69SVladimir Kotal """ 1492d57dc69SVladimir Kotal Make RESTful API call. Occurrence of the pattern in the URI 1502d57dc69SVladimir Kotal (first part of the command) or data payload will be replaced by the name. 1512d57dc69SVladimir Kotal 1522d57dc69SVladimir Kotal Default content type is application/json. 1532d57dc69SVladimir Kotal 15489229afdSVladimir Kotal :param command: command (list of URI, HTTP verb, data payload, 15589229afdSVladimir Kotal HTTP header dictionary) 1562d97c0a2SVladimir Kotal :param substitutions: dictionary of pattern:value for command and/or 1572d97c0a2SVladimir Kotal data substitution 15889229afdSVladimir Kotal :param http_headers: optional dictionary of HTTP headers to be appended 159732be1c2SVladimir Kotal :param timeout: optional timeout in seconds for API call response 16016e733aaSVladimir Kotal :param api_timeout: optional timeout in seconds for asynchronous API call 161959b34e9SVladimir Kotal :return value from given requests method 1622d57dc69SVladimir Kotal """ 1632d57dc69SVladimir Kotal 1642d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 1652d57dc69SVladimir Kotal 1662d57dc69SVladimir Kotal if not isinstance(command, dict) or command.get(COMMAND_PROPERTY) is None: 1672d57dc69SVladimir Kotal raise Exception("invalid command") 1682d57dc69SVladimir Kotal 1692d57dc69SVladimir Kotal command = command[COMMAND_PROPERTY] 1702d57dc69SVladimir Kotal 1712d57dc69SVladimir Kotal uri, verb, data, *_ = command 1722d57dc69SVladimir Kotal try: 1732d57dc69SVladimir Kotal headers = command[3] 1742d57dc69SVladimir Kotal if headers and not isinstance(headers, dict): 1752d57dc69SVladimir Kotal raise Exception("headers must be a dictionary") 1762d57dc69SVladimir Kotal except IndexError: 1772d57dc69SVladimir Kotal headers = {} 1782d57dc69SVladimir Kotal 1792d57dc69SVladimir Kotal if headers is None: 1802d57dc69SVladimir Kotal headers = {} 1812d57dc69SVladimir Kotal 182b369c884SVladimir Kotal logger.debug("Headers from the command: {}".format(headers)) 18389229afdSVladimir Kotal if http_headers: 184b369c884SVladimir Kotal logger.debug("Updating HTTP headers for command {} with {}". 185b369c884SVladimir Kotal format(command, http_headers)) 18689229afdSVladimir Kotal headers.update(http_headers) 18789229afdSVladimir Kotal 1882d97c0a2SVladimir Kotal uri = subst(uri, substitutions) 1892d57dc69SVladimir Kotal header_names = [x.lower() for x in headers.keys()] 1902d57dc69SVladimir Kotal 1912d57dc69SVladimir Kotal if data: 1922d57dc69SVladimir Kotal if CONTENT_TYPE.lower() not in header_names: 1932d57dc69SVladimir Kotal logger.debug("Adding header: {} = {}". 1942d57dc69SVladimir Kotal format(CONTENT_TYPE, APPLICATION_JSON)) 1952d57dc69SVladimir Kotal headers[CONTENT_TYPE] = APPLICATION_JSON 1962d57dc69SVladimir Kotal 1972d57dc69SVladimir Kotal for (k, v) in headers.items(): 1982d57dc69SVladimir Kotal if k.lower() == CONTENT_TYPE.lower(): 1992d57dc69SVladimir Kotal if headers[k].lower() == APPLICATION_JSON.lower(): 2002d57dc69SVladimir Kotal logger.debug("Converting {} to JSON".format(data)) 2012d57dc69SVladimir Kotal data = json.dumps(data) 2022d57dc69SVladimir Kotal break 2032d57dc69SVladimir Kotal 2042d97c0a2SVladimir Kotal data = subst(data, substitutions) 2052d57dc69SVladimir Kotal logger.debug("entity data: {}".format(data)) 2062d57dc69SVladimir Kotal 20716e733aaSVladimir Kotal return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout, api_timeout=api_timeout) 208