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# 21*cc7022c6SVladimir 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 371c258122SVladimir Kotaldef wait_for_async_api(response, api_timeout, headers=None, timeout=None): 381c258122SVladimir Kotal """ 391c258122SVladimir Kotal :param response: request 401c258122SVladimir Kotal :param api_timeout: asynchronous API timeout 411c258122SVladimir Kotal :param headers: request headers 421c258122SVladimir Kotal :param timeout: connect timeout 431c258122SVladimir Kotal :return: request 441c258122SVladimir Kotal """ 451c258122SVladimir Kotal logger = logging.getLogger(__name__) 461c258122SVladimir Kotal 471c258122SVladimir Kotal location_uri = response.headers.get("Location") 481c258122SVladimir Kotal if location_uri is None: 491c258122SVladimir Kotal raise Exception(f"no Location header in {response}") 501c258122SVladimir Kotal 51*cc7022c6SVladimir Kotal start_time = time.time() 521c258122SVladimir Kotal for _ in range(api_timeout): 531c258122SVladimir Kotal logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}") 541c258122SVladimir Kotal response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 551c258122SVladimir Kotal if response is None: 561c258122SVladimir Kotal raise Exception("API call failed") 571c258122SVladimir Kotal 581c258122SVladimir Kotal if response.status_code == 202: 591c258122SVladimir Kotal time.sleep(1) 601c258122SVladimir Kotal else: 611c258122SVladimir Kotal break 621c258122SVladimir Kotal 631c258122SVladimir Kotal if response.status_code == 202: 64*cc7022c6SVladimir Kotal wait_time = time.time() - start_time 65*cc7022c6SVladimir Kotal logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}") 661c258122SVladimir Kotal return response 671c258122SVladimir Kotal 681c258122SVladimir Kotal logger.debug(f"DELETE API call to {location_uri}") 691c258122SVladimir Kotal requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 701c258122SVladimir Kotal 711c258122SVladimir Kotal return response 721c258122SVladimir Kotal 731c258122SVladimir Kotal 74*cc7022c6SVladimir Kotaldef do_api_call(verb, uri, params=None, headers=None, data=None, timeout=60, api_timeout=300): 752d57dc69SVladimir Kotal """ 762d57dc69SVladimir Kotal Perform an API call. Will raise an exception if the request fails. 772d57dc69SVladimir Kotal :param verb: string holding HTTP verb 782d57dc69SVladimir Kotal :param uri: URI string 792d57dc69SVladimir Kotal :param params: request parameters 802d57dc69SVladimir Kotal :param headers: HTTP headers dictionary 812d57dc69SVladimir Kotal :param data: data or None 82732be1c2SVladimir Kotal :param timeout: optional connect timeout in seconds. 831c258122SVladimir Kotal Applies also to asynchronous API status calls. 84732be1c2SVladimir Kotal If None, default (60 seconds) will be used. 851c258122SVladimir Kotal :param api_timeout: optional timeout for asynchronous API requests in seconds. 861c258122SVladimir Kotal If None, default (300 seconds) will be used. 872d57dc69SVladimir Kotal :return: the result of the handler call, can be None 882d57dc69SVladimir Kotal """ 892d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 902d57dc69SVladimir Kotal 912d57dc69SVladimir Kotal handler = getattr(requests, verb.lower()) 922d57dc69SVladimir Kotal if handler is None or not callable(handler): 932d57dc69SVladimir Kotal raise Exception('Unknown HTTP verb: {}'.format(verb)) 942d57dc69SVladimir Kotal 95*cc7022c6SVladimir Kotal logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}". 96*cc7022c6SVladimir Kotal format(verb, uri, data, timeout, api_timeout, headers)) 972d57dc69SVladimir Kotal r = handler( 982d57dc69SVladimir Kotal uri, 992d57dc69SVladimir Kotal data=data, 1002d57dc69SVladimir Kotal params=params, 1012d57dc69SVladimir Kotal headers=headers, 102732be1c2SVladimir Kotal proxies=get_proxies(uri), 103732be1c2SVladimir Kotal timeout=timeout 1042d57dc69SVladimir Kotal ) 1052d57dc69SVladimir Kotal 1062d57dc69SVladimir Kotal if r is None: 1072d57dc69SVladimir Kotal raise Exception("API call failed") 1082d57dc69SVladimir Kotal 1091c258122SVladimir Kotal if r.status_code == 202: 1101c258122SVladimir Kotal r = wait_for_async_api(r, api_timeout, headers=headers, timeout=timeout) 1111c258122SVladimir Kotal 1122d57dc69SVladimir Kotal r.raise_for_status() 1132d57dc69SVladimir Kotal 1142d57dc69SVladimir Kotal return r 1152d57dc69SVladimir Kotal 1162d57dc69SVladimir Kotal 1172d97c0a2SVladimir Kotaldef subst(src, substitutions): 1182d97c0a2SVladimir Kotal if substitutions: 1192d97c0a2SVladimir Kotal for pattern, value in substitutions.items(): 1202d97c0a2SVladimir Kotal if value: 1212d97c0a2SVladimir Kotal src = src.replace(pattern, value) 1222d97c0a2SVladimir Kotal 1232d97c0a2SVladimir Kotal return src 1242d97c0a2SVladimir Kotal 1252d97c0a2SVladimir Kotal 126732be1c2SVladimir Kotaldef call_rest_api(command, substitutions=None, http_headers=None, timeout=None): 1272d57dc69SVladimir Kotal """ 1282d57dc69SVladimir Kotal Make RESTful API call. Occurrence of the pattern in the URI 1292d57dc69SVladimir Kotal (first part of the command) or data payload will be replaced by the name. 1302d57dc69SVladimir Kotal 1312d57dc69SVladimir Kotal Default content type is application/json. 1322d57dc69SVladimir Kotal 13389229afdSVladimir Kotal :param command: command (list of URI, HTTP verb, data payload, 13489229afdSVladimir Kotal HTTP header dictionary) 1352d97c0a2SVladimir Kotal :param substitutions: dictionary of pattern:value for command and/or 1362d97c0a2SVladimir Kotal data substitution 13789229afdSVladimir Kotal :param http_headers: optional dictionary of HTTP headers to be appended 138732be1c2SVladimir Kotal :param timeout: optional timeout in seconds for API call response 1392d57dc69SVladimir Kotal :return return value from given requests method 1402d57dc69SVladimir Kotal """ 1412d57dc69SVladimir Kotal 1422d57dc69SVladimir Kotal logger = logging.getLogger(__name__) 1432d57dc69SVladimir Kotal 1442d57dc69SVladimir Kotal if not isinstance(command, dict) or command.get(COMMAND_PROPERTY) is None: 1452d57dc69SVladimir Kotal raise Exception("invalid command") 1462d57dc69SVladimir Kotal 1472d57dc69SVladimir Kotal command = command[COMMAND_PROPERTY] 1482d57dc69SVladimir Kotal 1492d57dc69SVladimir Kotal uri, verb, data, *_ = command 1502d57dc69SVladimir Kotal try: 1512d57dc69SVladimir Kotal headers = command[3] 1522d57dc69SVladimir Kotal if headers and not isinstance(headers, dict): 1532d57dc69SVladimir Kotal raise Exception("headers must be a dictionary") 1542d57dc69SVladimir Kotal except IndexError: 1552d57dc69SVladimir Kotal headers = {} 1562d57dc69SVladimir Kotal 1572d57dc69SVladimir Kotal if headers is None: 1582d57dc69SVladimir Kotal headers = {} 1592d57dc69SVladimir Kotal 160b369c884SVladimir Kotal logger.debug("Headers from the command: {}".format(headers)) 16189229afdSVladimir Kotal if http_headers: 162b369c884SVladimir Kotal logger.debug("Updating HTTP headers for command {} with {}". 163b369c884SVladimir Kotal format(command, http_headers)) 16489229afdSVladimir Kotal headers.update(http_headers) 16589229afdSVladimir Kotal 1662d97c0a2SVladimir Kotal uri = subst(uri, substitutions) 1672d57dc69SVladimir Kotal header_names = [x.lower() for x in headers.keys()] 1682d57dc69SVladimir Kotal 1692d57dc69SVladimir Kotal if data: 1702d57dc69SVladimir Kotal if CONTENT_TYPE.lower() not in header_names: 1712d57dc69SVladimir Kotal logger.debug("Adding header: {} = {}". 1722d57dc69SVladimir Kotal format(CONTENT_TYPE, APPLICATION_JSON)) 1732d57dc69SVladimir Kotal headers[CONTENT_TYPE] = APPLICATION_JSON 1742d57dc69SVladimir Kotal 1752d57dc69SVladimir Kotal for (k, v) in headers.items(): 1762d57dc69SVladimir Kotal if k.lower() == CONTENT_TYPE.lower(): 1772d57dc69SVladimir Kotal if headers[k].lower() == APPLICATION_JSON.lower(): 1782d57dc69SVladimir Kotal logger.debug("Converting {} to JSON".format(data)) 1792d57dc69SVladimir Kotal data = json.dumps(data) 1802d57dc69SVladimir Kotal break 1812d57dc69SVladimir Kotal 1822d97c0a2SVladimir Kotal data = subst(data, substitutions) 1832d57dc69SVladimir Kotal logger.debug("entity data: {}".format(data)) 1842d57dc69SVladimir Kotal 185732be1c2SVladimir Kotal return do_api_call(verb, uri, headers=headers, data=data, timeout=timeout) 186