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 |
1 |
import copy |
18 |
1 |
import json |
19 |
1 |
from time import time |
20 |
|
|
21 |
1 |
import requests |
22 |
1 |
from requests.exceptions import ConnectionError |
23 |
|
|
24 |
|
|
25 |
1 |
class HttpException(Exception): |
26 |
1 |
pass |
27 |
|
|
28 |
|
|
29 |
1 |
class NotFound(HttpException): |
30 |
1 |
pass |
31 |
|
|
32 |
|
|
33 |
1 |
class AuthError(HttpException): |
34 |
1 |
pass |
35 |
|
|
36 |
|
|
37 |
1 |
class DuplicateFound(HttpException): |
38 |
1 |
pass |
39 |
|
|
40 |
|
|
41 |
1 |
class ServiceUnavailableException(HttpException): |
42 |
1 |
pass |
43 |
|
|
44 |
|
|
45 |
1 |
class ContrailHttp(object): |
46 |
1 |
def __init__(self, auth_info, logger, verify): |
47 |
1 |
self._logger = logger |
48 |
|
# Verify client cert |
49 |
1 |
self.ssl_verify = verify |
50 |
|
# auth info: must contain auth_url and auth_dict |
51 |
1 |
self.auth_url = auth_info["auth_url"] |
52 |
1 |
self.auth_dict = auth_info["auth_dict"] |
53 |
|
|
54 |
1 |
self.max_retries = 3 |
55 |
|
|
56 |
|
# Default token timeout |
57 |
1 |
self.token_timeout = 3500 |
58 |
1 |
self.token = None |
59 |
|
# TODO - improve configuration timeouts |
60 |
|
|
61 |
1 |
def get_cmd(self, url, headers): |
62 |
0 |
self._logger.debug("") |
63 |
0 |
resp = self._request("GET", url, headers) |
64 |
|
|
65 |
0 |
return resp.json() |
66 |
|
|
67 |
1 |
def post_headers_cmd(self, url, headers, post_fields_dict=None): |
68 |
0 |
self._logger.debug("") |
69 |
|
|
70 |
|
# obfuscate password before logging dict |
71 |
0 |
if ( |
72 |
|
post_fields_dict.get("auth", {}) |
73 |
|
.get("identity", {}) |
74 |
|
.get("password", {}) |
75 |
|
.get("user", {}) |
76 |
|
.get("password") |
77 |
|
): |
78 |
0 |
post_fields_dict_copy = copy.deepcopy(post_fields_dict) |
79 |
0 |
post_fields_dict["auth"]["identity"]["password"]["user"][ |
80 |
|
"password" |
81 |
|
] = "******" |
82 |
0 |
json_data_log = post_fields_dict_copy |
83 |
|
else: |
84 |
0 |
json_data_log = post_fields_dict |
85 |
|
|
86 |
0 |
self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log))) |
87 |
0 |
resp = self._request("POST_HEADERS", url, headers, data=post_fields_dict) |
88 |
|
|
89 |
0 |
return resp.text |
90 |
|
|
91 |
1 |
def post_cmd(self, url, headers, post_fields_dict=None): |
92 |
0 |
self._logger.debug("") |
93 |
|
|
94 |
|
# obfuscate password before logging dict |
95 |
0 |
if ( |
96 |
|
post_fields_dict.get("auth", {}) |
97 |
|
.get("identity", {}) |
98 |
|
.get("password", {}) |
99 |
|
.get("user", {}) |
100 |
|
.get("password") |
101 |
|
): |
102 |
0 |
post_fields_dict_copy = copy.deepcopy(post_fields_dict) |
103 |
0 |
post_fields_dict["auth"]["identity"]["password"]["user"][ |
104 |
|
"password" |
105 |
|
] = "******" |
106 |
0 |
json_data_log = post_fields_dict_copy |
107 |
|
else: |
108 |
0 |
json_data_log = post_fields_dict |
109 |
|
|
110 |
0 |
self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log))) |
111 |
0 |
resp = self._request("POST", url, headers, data=post_fields_dict) |
112 |
|
|
113 |
0 |
return resp.text |
114 |
|
|
115 |
1 |
def delete_cmd(self, url, headers): |
116 |
0 |
self._logger.debug("") |
117 |
0 |
resp = self._request("DELETE", url, headers) |
118 |
|
|
119 |
0 |
return resp.text |
120 |
|
|
121 |
1 |
def _get_token(self, headers): |
122 |
0 |
if self.auth_url: |
123 |
0 |
self._logger.debug("Current Token: {}".format(self.token)) |
124 |
0 |
auth_url = self.auth_url + "auth/tokens" |
125 |
|
|
126 |
0 |
if self.token is None or self._token_expired(): |
127 |
0 |
if not self.auth_url: |
128 |
0 |
self.token = "" |
129 |
|
|
130 |
0 |
resp = self._request_noauth( |
131 |
|
url=auth_url, op="POST", headers=headers, data=self.auth_dict |
132 |
|
) |
133 |
0 |
self.token = resp.headers.get("x-subject-token") |
134 |
0 |
self.last_token_time = time.time() |
135 |
0 |
self._logger.debug("Obtained token: {}".format(self.token)) |
136 |
|
|
137 |
0 |
return self.token |
138 |
|
|
139 |
1 |
def _token_expired(self): |
140 |
0 |
current_time = time.time() |
141 |
|
|
142 |
0 |
if self.last_token_time and ( |
143 |
|
current_time - self.last_token_time < self.token_timeout |
144 |
|
): |
145 |
0 |
return False |
146 |
|
else: |
147 |
0 |
return True |
148 |
|
|
149 |
1 |
def _request(self, op, url, http_headers, data=None, retry_auth_error=True): |
150 |
0 |
headers = http_headers.copy() |
151 |
|
|
152 |
|
# Get authorization (include authentication headers) |
153 |
|
# TODO add again token |
154 |
|
# token = self._get_token(headers) |
155 |
0 |
token = None |
156 |
|
|
157 |
0 |
if token: |
158 |
0 |
headers["X-Auth-Token"] = token |
159 |
|
|
160 |
0 |
try: |
161 |
0 |
return self._request_noauth(op, url, headers, data) |
162 |
0 |
except AuthError: |
163 |
|
# If there is an auth error retry just once |
164 |
0 |
if retry_auth_error: |
165 |
0 |
return self._request( |
166 |
|
self, op, url, headers, data, retry_auth_error=False |
167 |
|
) |
168 |
|
|
169 |
1 |
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 |
0 |
retry = 0 |
178 |
0 |
while retry < self.max_retries: |
179 |
0 |
retry += 1 |
180 |
|
|
181 |
|
# Execute operation |
182 |
0 |
try: |
183 |
0 |
self._logger.info("Request METHOD: {} URL: {}".format(op, url)) |
184 |
0 |
if op == "GET": |
185 |
0 |
resp = self._http_get(url, headers, query_params=data) |
186 |
0 |
elif op == "POST": |
187 |
0 |
resp = self._http_post(url, headers, json_data=data) |
188 |
0 |
elif op == "POST_HEADERS": |
189 |
0 |
resp = self._http_post_headers(url, headers, json_data=data) |
190 |
0 |
elif op == "DELETE": |
191 |
0 |
resp = self._http_delete(url, headers, json_data=data) |
192 |
|
else: |
193 |
0 |
raise HttpException("Unsupported operation: {}".format(op)) |
194 |
|
|
195 |
0 |
self._logger.info("Response HTTPCODE: {}".format(resp.status_code)) |
196 |
|
|
197 |
|
# Check http return code |
198 |
0 |
if resp: |
199 |
0 |
return resp |
200 |
|
else: |
201 |
0 |
status_code = resp.status_code |
202 |
0 |
if status_code == 401: |
203 |
|
# Auth Error - set token to None to reload it and raise AuthError |
204 |
0 |
self.token = None |
205 |
|
|
206 |
0 |
raise AuthError("Auth error executing operation") |
207 |
0 |
elif status_code == 409: |
208 |
0 |
raise DuplicateFound( |
209 |
|
"Duplicate resource url: {}, response: {}".format( |
210 |
|
url, resp.text |
211 |
|
) |
212 |
|
) |
213 |
0 |
elif status_code == 404: |
214 |
0 |
raise NotFound( |
215 |
|
"Not found resource url: {}, response: {}".format( |
216 |
|
url, resp.text |
217 |
|
) |
218 |
|
) |
219 |
0 |
elif resp.status_code in [502, 503]: |
220 |
0 |
if not self.max_retries or retry >= self.max_retries: |
221 |
0 |
raise ServiceUnavailableException( |
222 |
|
"Service unavailable error url: {}".format(url) |
223 |
|
) |
224 |
|
|
225 |
0 |
continue |
226 |
|
else: |
227 |
0 |
raise HttpException( |
228 |
|
"Error status_code: {}, error_text: {}".format( |
229 |
|
resp.status_code, resp.text |
230 |
|
) |
231 |
|
) |
232 |
|
|
233 |
0 |
except ConnectionError as e: |
234 |
0 |
self._logger.error( |
235 |
|
"Connection error executing request: {}".format(repr(e)) |
236 |
|
) |
237 |
|
|
238 |
0 |
if not self.max_retries or retry >= self.max_retries: |
239 |
0 |
raise ConnectionError |
240 |
|
|
241 |
0 |
continue |
242 |
0 |
except Exception as e: |
243 |
0 |
self._logger.error("Error executing request: {}".format(repr(e))) |
244 |
0 |
raise e |
245 |
|
|
246 |
1 |
def _http_get(self, url, headers, query_params=None): |
247 |
0 |
return requests.get(url, headers=headers, params=query_params) |
248 |
|
|
249 |
1 |
def _http_post_headers(self, url, headers, json_data=None): |
250 |
0 |
return requests.head( |
251 |
|
url, json=json_data, headers=headers, verify=self.ssl_verify |
252 |
|
) |
253 |
|
|
254 |
1 |
def _http_post(self, url, headers, json_data=None): |
255 |
0 |
return requests.post( |
256 |
|
url, json=json_data, headers=headers, verify=self.ssl_verify |
257 |
|
) |
258 |
|
|
259 |
1 |
def _http_delete(self, url, headers, json_data=None): |
260 |
0 |
return requests.delete(url, json=json_data, headers=headers) |