1# 2# CDDL HEADER START 3# 4# The contents of this file are subject to the terms of the 5# Common Development and Distribution License (the "License"). 6# You may not use this file except in compliance with the License. 7# 8# See LICENSE.txt included in this distribution for the specific 9# language governing permissions and limitations under the License. 10# 11# When distributing Covered Code, include this CDDL HEADER in each 12# file and include the License file at LICENSE.txt. 13# If applicable, add the following below this CDDL HEADER, with the 14# fields enclosed by brackets "[]" replaced with your own identifying 15# information: Portions Copyright [yyyy] [name of copyright owner] 16# 17# CDDL HEADER END 18# 19 20# 21# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. 22# 23 24import json 25import logging 26import time 27 28import requests 29 30from .webutil import get_proxies, is_web_uri 31 32CONTENT_TYPE = 'Content-Type' 33APPLICATION_JSON = 'application/json' # default 34 35 36def call_finished(location_uri, headers, timeout): 37 """ 38 :param location_uri: URI to check the status of API call 39 :param headers: HTTP headers 40 :param timeout: connect timeout 41 :return indication and response tuple 42 """ 43 logger = logging.getLogger(__name__) 44 45 logger.debug(f"GET API call: {location_uri}, timeout {timeout} seconds and headers: {headers}") 46 response = requests.get(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 47 if response is None: 48 raise Exception("API call failed") 49 50 response.raise_for_status() 51 if response.status_code == 202: 52 return False, response 53 else: 54 return True, response 55 56 57def wait_for_async_api(response, api_timeout=None, headers=None, timeout=None): 58 """ 59 :param response: request 60 :param api_timeout: asynchronous API timeout (will wait forever or until error if None) 61 :param headers: request headers 62 :param timeout: connect timeout 63 :return: request 64 """ 65 logger = logging.getLogger(__name__) 66 67 location_uri = response.headers.get("Location") 68 if location_uri is None: 69 raise Exception(f"no Location header in {response}") 70 71 start_time = time.time() 72 if api_timeout is None: 73 while True: 74 done, response = call_finished(location_uri, headers, timeout) 75 if done: 76 break 77 time.sleep(1) 78 else: 79 for _ in range(api_timeout): 80 done, response = call_finished(location_uri, headers, timeout) 81 if done: 82 break 83 time.sleep(1) 84 85 if response.status_code == 202: 86 wait_time = time.time() - start_time 87 logger.warn(f"API request still not completed after {int(wait_time)} seconds: {response}") 88 return response 89 90 logger.debug(f"DELETE API call to {location_uri}") 91 requests.delete(location_uri, headers=headers, proxies=get_proxies(location_uri), timeout=timeout) 92 93 return response 94 95 96def do_api_call(method, uri, params=None, headers=None, data=None, timeout=None, api_timeout=None): 97 """ 98 Perform an API call. Will raise an exception if the request fails. 99 :param method: string holding HTTP verb 100 :param uri: URI string 101 :param params: request parameters 102 :param headers: HTTP headers dictionary 103 :param data: data or None 104 :param timeout: optional connect timeout in seconds. 105 Applies also to asynchronous API status calls. 106 :param api_timeout: optional timeout for asynchronous API requests in seconds. 107 :return: the result of the handler call, can be None 108 """ 109 logger = logging.getLogger(__name__) 110 111 handler = getattr(requests, method.lower()) 112 if handler is None or not callable(handler): 113 raise Exception('Unknown HTTP method: {}'.format(method)) 114 115 logger.debug("{} API call: {} with data '{}', connect timeout {} seconds, API timeout {} seconds and headers: {}". 116 format(method, uri, data, timeout, api_timeout, headers)) 117 r = handler( 118 uri, 119 data=data, 120 params=params, 121 headers=headers, 122 proxies=get_proxies(uri), 123 timeout=timeout 124 ) 125 logger.debug(f"API call result: {r}") 126 127 if r is None: 128 raise Exception("API call failed") 129 130 if r.status_code == 202: 131 r = wait_for_async_api(r, api_timeout=api_timeout, headers=headers, timeout=timeout) 132 133 r.raise_for_status() 134 135 return r 136 137 138def subst(src, substitutions): 139 if substitutions: 140 for pattern, value in substitutions.items(): 141 if value: 142 src = src.replace(pattern, value) 143 144 return src 145 146 147def call_rest_api(call, substitutions=None, http_headers=None, timeout=None, api_timeout=None): 148 """ 149 Make REST API call. Occurrence of the pattern in the URI 150 (first part of the command) or data payload will be replaced by the name. 151 152 Default content type is application/json. 153 154 :param call: ApiCall object 155 :param substitutions: dictionary of pattern:value for command and/or 156 data substitution 157 :param http_headers: optional dictionary of HTTP headers to be appended 158 :param timeout: optional connect/read timeout in seconds for API call 159 :param api_timeout: optional timeout in seconds for total duration of asynchronous API call 160 :return value from given requests method 161 """ 162 163 logger = logging.getLogger(__name__) 164 165 logger.debug(f"Headers from the ApiCall object: {call.headers}") 166 headers = call.headers 167 if http_headers: 168 logger.debug("Updating HTTP headers for API call {} with {}". 169 format(call, http_headers)) 170 headers.update(http_headers) 171 172 logger.debug("Performing URI substitutions") 173 uri = subst(call.uri, substitutions) 174 logger.debug(f"URI after the substitutions: {uri}") 175 176 if not is_web_uri(uri): 177 raise Exception(f"not a valid URI: {uri}") 178 179 call_timeout = call.api_timeout 180 if call_timeout: 181 logger.debug(f"Setting connect/read API timeout based on the call to {call_timeout}") 182 timeout = call_timeout 183 184 call_api_timeout = call.async_api_timeout 185 if call_api_timeout: 186 logger.debug(f"Setting async API timeout based on the call to {call_api_timeout}") 187 api_timeout = call_api_timeout 188 189 data = call.data 190 if data: 191 header_names = [x.lower() for x in headers.keys()] 192 if CONTENT_TYPE.lower() not in header_names: 193 logger.debug("Adding HTTP header: {} = {}". 194 format(CONTENT_TYPE, APPLICATION_JSON)) 195 headers[CONTENT_TYPE] = APPLICATION_JSON 196 197 for (k, v) in headers.items(): 198 if k.lower() == CONTENT_TYPE.lower(): 199 if headers[k].lower() == APPLICATION_JSON.lower(): 200 logger.debug("Converting {} to JSON".format(data)) 201 data = json.dumps(data) 202 break 203 204 data = subst(data, substitutions) 205 logger.debug("entity data: {}".format(data)) 206 207 return do_api_call(call.method, uri, headers=headers, data=data, timeout=timeout, api_timeout=api_timeout) 208