4c04c1c95eb3c2a72b656ed5a9e2c28f0c9218a5
[osm/RO.git] / RO-SDN-juniper_contrail / osm_rosdn_juniper_contrail / rest_lib.py
1 # Copyright 2020 ETSI
2 #
3 # All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16
17 import copy
18 import json
19 from time import time
20
21 import requests
22 from requests.exceptions import ConnectionError
23
24
25 class HttpException(Exception):
26 pass
27
28
29 class NotFound(HttpException):
30 pass
31
32
33 class AuthError(HttpException):
34 pass
35
36
37 class DuplicateFound(HttpException):
38 pass
39
40
41 class ServiceUnavailableException(HttpException):
42 pass
43
44
45 class ContrailHttp(object):
46 def __init__(self, auth_info, logger):
47 self._logger = logger
48 # default don't verify client cert
49 self._ssl_verify = False
50 # auth info: must contain auth_url and auth_dict
51 self.auth_url = auth_info["auth_url"]
52 self.auth_dict = auth_info["auth_dict"]
53
54 self.max_retries = 3
55
56 # Default token timeout
57 self.token_timeout = 3500
58 self.token = None
59 # TODO - improve configuration timeouts
60
61 def get_cmd(self, url, headers):
62 self._logger.debug("")
63 resp = self._request("GET", url, headers)
64
65 return resp.json()
66
67 def post_headers_cmd(self, url, headers, post_fields_dict=None):
68 self._logger.debug("")
69
70 # obfuscate password before logging dict
71 if (
72 post_fields_dict.get("auth", {})
73 .get("identity", {})
74 .get("password", {})
75 .get("user", {})
76 .get("password")
77 ):
78 post_fields_dict_copy = copy.deepcopy(post_fields_dict)
79 post_fields_dict["auth"]["identity"]["password"]["user"][
80 "password"
81 ] = "******"
82 json_data_log = post_fields_dict_copy
83 else:
84 json_data_log = post_fields_dict
85
86 self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
87 resp = self._request("POST_HEADERS", url, headers, data=post_fields_dict)
88
89 return resp.text
90
91 def post_cmd(self, url, headers, post_fields_dict=None):
92 self._logger.debug("")
93
94 # obfuscate password before logging dict
95 if (
96 post_fields_dict.get("auth", {})
97 .get("identity", {})
98 .get("password", {})
99 .get("user", {})
100 .get("password")
101 ):
102 post_fields_dict_copy = copy.deepcopy(post_fields_dict)
103 post_fields_dict["auth"]["identity"]["password"]["user"][
104 "password"
105 ] = "******"
106 json_data_log = post_fields_dict_copy
107 else:
108 json_data_log = post_fields_dict
109
110 self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
111 resp = self._request("POST", url, headers, data=post_fields_dict)
112
113 return resp.text
114
115 def delete_cmd(self, url, headers):
116 self._logger.debug("")
117 resp = self._request("DELETE", url, headers)
118
119 return resp.text
120
121 def _get_token(self, headers):
122 if self.auth_url:
123 self._logger.debug("Current Token: {}".format(self.token))
124 auth_url = self.auth_url + "auth/tokens"
125
126 if self.token is None or self._token_expired():
127 if not self.auth_url:
128 self.token = ""
129
130 resp = self._request_noauth(
131 url=auth_url, op="POST", headers=headers, data=self.auth_dict
132 )
133 self.token = resp.headers.get("x-subject-token")
134 self.last_token_time = time.time()
135 self._logger.debug("Obtained token: {}".format(self.token))
136
137 return self.token
138
139 def _token_expired(self):
140 current_time = time.time()
141
142 if self.last_token_time and (
143 current_time - self.last_token_time < self.token_timeout
144 ):
145 return False
146 else:
147 return True
148
149 def _request(self, op, url, http_headers, data=None, retry_auth_error=True):
150 headers = http_headers.copy()
151
152 # Get authorization (include authentication headers)
153 # TODO add again token
154 # token = self._get_token(headers)
155 token = None
156
157 if token:
158 headers["X-Auth-Token"] = token
159
160 try:
161 return self._request_noauth(op, url, headers, data)
162 except AuthError:
163 # If there is an auth error retry just once
164 if retry_auth_error:
165 return self._request(
166 self, op, url, headers, data, retry_auth_error=False
167 )
168
169 def _request_noauth(self, op, url, headers, data=None):
170 # Method to execute http requests with error control
171 # Authentication error, always make just one retry
172 # ConnectionError or ServiceUnavailable make configured retries with sleep between them
173 # Other errors to raise:
174 # - NotFound
175 # - Conflict
176
177 retry = 0
178 while retry < self.max_retries:
179 retry += 1
180
181 # Execute operation
182 try:
183 self._logger.info("Request METHOD: {} URL: {}".format(op, url))
184 if op == "GET":
185 resp = self._http_get(url, headers, query_params=data)
186 elif op == "POST":
187 resp = self._http_post(url, headers, json_data=data)
188 elif op == "POST_HEADERS":
189 resp = self._http_post_headers(url, headers, json_data=data)
190 elif op == "DELETE":
191 resp = self._http_delete(url, headers, json_data=data)
192 else:
193 raise HttpException("Unsupported operation: {}".format(op))
194
195 self._logger.info("Response HTTPCODE: {}".format(resp.status_code))
196
197 # Check http return code
198 if resp:
199 return resp
200 else:
201 status_code = resp.status_code
202 if status_code == 401:
203 # Auth Error - set token to None to reload it and raise AuthError
204 self.token = None
205
206 raise AuthError("Auth error executing operation")
207 elif status_code == 409:
208 raise DuplicateFound(
209 "Duplicate resource url: {}, response: {}".format(
210 url, resp.text
211 )
212 )
213 elif status_code == 404:
214 raise NotFound(
215 "Not found resource url: {}, response: {}".format(
216 url, resp.text
217 )
218 )
219 elif resp.status_code in [502, 503]:
220 if not self.max_retries or retry >= self.max_retries:
221 raise ServiceUnavailableException(
222 "Service unavailable error url: {}".format(url)
223 )
224
225 continue
226 else:
227 raise HttpException(
228 "Error status_code: {}, error_text: {}".format(
229 resp.status_code, resp.text
230 )
231 )
232
233 except ConnectionError as e:
234 self._logger.error(
235 "Connection error executing request: {}".format(repr(e))
236 )
237
238 if not self.max_retries or retry >= self.max_retries:
239 raise ConnectionError
240
241 continue
242 except Exception as e:
243 self._logger.error("Error executing request: {}".format(repr(e)))
244 raise e
245
246 def _http_get(self, url, headers, query_params=None):
247 return requests.get(url, headers=headers, params=query_params)
248
249 def _http_post_headers(self, url, headers, json_data=None):
250 return requests.head(url, json=json_data, headers=headers, verify=False)
251
252 def _http_post(self, url, headers, json_data=None):
253 return requests.post(url, json=json_data, headers=headers, verify=False)
254
255 def _http_delete(self, url, headers, json_data=None):
256 return requests.delete(url, json=json_data, headers=headers)