Fix Tap failing tests
[csit.git] / resources / libraries / python / HTTPRequest.py
1 # Copyright (c) 2018 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Implementation of HTTP requests GET, PUT, POST and DELETE used in
15 communication with Honeycomb.
16
17 The HTTP requests are implemented in the class HTTPRequest which uses
18 requests.request.
19 """
20
21 from ipaddress import IPv6Address, AddressValueError
22 from enum import IntEnum, unique
23
24 from robot.api.deco import keyword
25 from robot.api import logger
26 from robot.libraries.BuiltIn import BuiltIn
27
28 from requests import request, RequestException, Timeout, TooManyRedirects, \
29     HTTPError, ConnectionError
30 from requests.auth import HTTPBasicAuth
31
32
33 @unique
34 class HTTPCodes(IntEnum):
35     """HTTP status codes"""
36     OK = 200  # HTTP standard code name.  # pylint: disable=invalid-name
37     ACCEPTED = 201
38     UNAUTHORIZED = 401
39     FORBIDDEN = 403
40     NOT_FOUND = 404
41     CONFLICT = 409
42     INTERNAL_SERVER_ERROR = 500
43     SERVICE_UNAVAILABLE = 503
44
45
46 class HTTPRequestError(Exception):
47     """Exception raised by HTTPRequest objects.
48
49     When raising this exception, put this information to the message in this
50     order:
51
52      - short description of the encountered problem,
53      - relevant messages if there are any collected, e.g., from caught
54        exception,
55      - relevant data if there are any collected.
56
57     The logging is performed on two levels: 1. error - short description of the
58     problem; 2. debug - detailed information.
59     """
60
61     def __init__(self, msg, details='', enable_logging=True):
62         """Sets the exception message and enables / disables logging.
63
64         It is not wanted to log errors when using these keywords together
65         with keywords like "Wait until keyword succeeds". So you can disable
66         logging by setting enable_logging to False.
67
68         :param msg: Message to be displayed and logged.
69         :param enable_logging: When True, logging is enabled, otherwise
70             logging is disabled.
71         :type msg: str
72         :type enable_logging: bool
73         """
74         super(HTTPRequestError, self).__init__()
75         self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
76         self._details = details
77         if enable_logging:
78             logger.info(self._msg)
79             logger.debug(self._details)
80
81     def __repr__(self):
82         return repr(self._msg)
83
84     def __str__(self):
85         return str(self._msg)
86
87
88 class HTTPRequest(object):
89     """A class implementing HTTP requests GET, PUT, POST and DELETE used in
90     communication with Honeycomb.
91
92     The communication with Honeycomb and processing of all exceptions is done in
93     the method _http_request which uses requests.request to send requests and
94     receive responses. The received status code and content of response are
95     logged on the debug level.
96     All possible exceptions raised by requests.request are also processed there.
97
98     The other methods (get, put, post and delete) use _http_request to send
99     corresponding request.
100
101     These methods must not be used as keywords in tests. Use keywords
102     implemented in the module HoneycombAPIKeywords instead.
103     """
104
105     def __init__(self):
106         pass
107
108     @staticmethod
109     def create_full_url(ip_addr, port, path):
110         """Creates full url including host, port, and path to data.
111
112         :param ip_addr: Server IP.
113         :param port: Communication port.
114         :param path: Path to data.
115         :type ip_addr: str
116         :type port: str or int
117         :type path: str
118         :returns: Full url.
119         :rtype: str
120         """
121
122         try:
123             IPv6Address(unicode(ip_addr))
124             # IPv6 address must be in brackets
125             ip_addr = "[{0}]".format(ip_addr)
126         except (AttributeError, AddressValueError):
127             pass
128
129         return "http://{ip}:{port}{path}".format(ip=ip_addr, port=port,
130                                                  path=path)
131
132     @staticmethod
133     def _http_request(method, node, path, enable_logging=True, **kwargs):
134         """Sends specified HTTP request and returns status code and response
135         content.
136
137         :param method: The method to be performed on the resource identified by
138         the given request URI.
139         :param node: Honeycomb node.
140         :param path: URL path, e.g. /index.html.
141         :param enable_logging: Used to suppress errors when checking Honeycomb
142         state during suite setup and teardown.
143         :param kwargs: Named parameters accepted by request.request:
144             params -- (optional) Dictionary or bytes to be sent in the query
145             string for the Request.
146             data -- (optional) Dictionary, bytes, or file-like object to
147             send in the body of the Request.
148             json -- (optional) json data to send in the body of the Request.
149             headers -- (optional) Dictionary of HTTP Headers to send with
150             the Request.
151             cookies -- (optional) Dict or CookieJar object to send with the
152             Request.
153             files -- (optional) Dictionary of 'name': file-like-objects
154             (or {'name': ('filename', fileobj)}) for multipart encoding upload.
155             timeout (float or tuple) -- (optional) How long to wait for the
156             server to send data before giving up, as a float, or a (connect
157             timeout, read timeout) tuple.
158             allow_redirects (bool) -- (optional) Boolean. Set to True if POST/
159             PUT/DELETE redirect following is allowed.
160             proxies -- (optional) Dictionary mapping protocol to the URL of
161             the proxy.
162             verify -- (optional) whether the SSL cert will be verified.
163             A CA_BUNDLE path can also be provided. Defaults to True.
164             stream -- (optional) if False, the response content will be
165             immediately downloaded.
166             cert -- (optional) if String, path to ssl client cert file (.pem).
167             If Tuple, ('cert', 'key') pair.
168         :type method: str
169         :type node: dict
170         :type path: str
171         :type enable_logging: bool
172         :type kwargs: dict
173         :returns: Status code and content of response.
174         :rtype: tuple
175         :raises HTTPRequestError: If
176         1. it is not possible to connect,
177         2. invalid HTTP response comes from server,
178         3. request exceeded the configured number of maximum re-directions,
179         4. request timed out,
180         5. there is any other unexpected HTTP request exception.
181         """
182         timeout = kwargs["timeout"]
183
184         use_odl = BuiltIn().get_variable_value("${use_odl_client}")
185
186         if use_odl:
187             port = 8181
188             # Using default ODL Restconf port
189             # TODO: add node["honeycomb"]["odl_port"] to topology, use it here
190             odl_url_part = "/network-topology:network-topology/topology/" \
191                            "topology-netconf/node/vpp/yang-ext:mount"
192         else:
193             port = node["honeycomb"]["port"]
194             odl_url_part = ""
195
196         try:
197             path = path.format(odl_url_part=odl_url_part)
198         except KeyError:
199             pass
200
201         url = HTTPRequest.create_full_url(node['host'],
202                                           port,
203                                           path)
204         try:
205             auth = HTTPBasicAuth(node['honeycomb']['user'],
206                                  node['honeycomb']['passwd'])
207             rsp = request(method, url, auth=auth, verify=False, **kwargs)
208
209             logger.debug("Status code: {0}".format(rsp.status_code))
210             logger.debug("Response: {0}".format(rsp.content))
211
212             return rsp.status_code, rsp.content
213
214         except ConnectionError as err:
215             # Switching the logging on / off is needed only for
216             # "requests.ConnectionError"
217             raise HTTPRequestError("Not possible to connect to {0}:{1}.".
218                                    format(node['host'],
219                                           node['honeycomb']['port']),
220                                    repr(err), enable_logging=enable_logging)
221         except HTTPError as err:
222             raise HTTPRequestError("Invalid HTTP response from {0}.".
223                                    format(node['host']), repr(err))
224         except TooManyRedirects as err:
225             raise HTTPRequestError("Request exceeded the configured number "
226                                    "of maximum re-directions.", repr(err))
227         except Timeout as err:
228             raise HTTPRequestError("Request timed out. Timeout is set to {0}.".
229                                    format(timeout), repr(err))
230         except RequestException as err:
231             raise HTTPRequestError("Unexpected HTTP request exception.",
232                                    repr(err))
233
234     @staticmethod
235     @keyword(name="HTTP Get")
236     def get(node, path, headers=None, timeout=15, enable_logging=True):
237         """Sends a GET request and returns the response and status code.
238
239         :param node: Honeycomb node.
240         :param path: URL path, e.g. /index.html.
241         :param headers: Dictionary of HTTP Headers to send with the Request.
242         :param timeout: How long to wait for the server to send data before
243             giving up, as a float, or a (connect timeout, read timeout) tuple.
244         :param enable_logging: Used to suppress errors when checking Honeycomb
245             state during suite setup and teardown. When True,
246             logging is enabled, otherwise logging is disabled.
247         :type node: dict
248         :type path: str
249         :type headers: dict
250         :type timeout: float or tuple
251         :type enable_logging: bool
252         :returns: Status code and content of response.
253         :rtype: tuple
254         """
255
256         return HTTPRequest._http_request('GET', node, path,
257                                          enable_logging=enable_logging,
258                                          headers=headers, timeout=timeout)
259
260     @staticmethod
261     @keyword(name="HTTP Put")
262     def put(node, path, headers=None, payload=None, json=None, timeout=15):
263         """Sends a PUT request and returns the response and status code.
264
265         :param node: Honeycomb node.
266         :param path: URL path, e.g. /index.html.
267         :param headers: Dictionary of HTTP Headers to send with the Request.
268         :param payload: Dictionary, bytes, or file-like object to send in
269             the body of the Request.
270         :param json: JSON formatted string to send in the body of the Request.
271         :param timeout: How long to wait for the server to send data before
272             giving up, as a float, or a (connect timeout, read timeout) tuple.
273         :type node: dict
274         :type path: str
275         :type headers: dict
276         :type payload: dict, bytes, or file-like object
277         :type json: str
278         :type timeout: float or tuple
279         :returns: Status code and content of response.
280         :rtype: tuple
281         """
282         return HTTPRequest._http_request('PUT', node, path, headers=headers,
283                                          data=payload, json=json,
284                                          timeout=timeout)
285
286     @staticmethod
287     @keyword(name="HTTP Post")
288     def post(node, path, headers=None, payload=None, json=None, timeout=15,
289              enable_logging=True):
290         """Sends a POST request and returns the response and status code.
291
292         :param node: Honeycomb node.
293         :param path: URL path, e.g. /index.html.
294         :param headers: Dictionary of HTTP Headers to send with the Request.
295         :param payload: Dictionary, bytes, or file-like object to send in
296             the body of the Request.
297         :param json: JSON formatted string to send in the body of the Request.
298         :param timeout: How long to wait for the server to send data before
299             giving up, as a float, or a (connect timeout, read timeout) tuple.
300         :param enable_logging: Used to suppress errors when checking ODL
301             state during suite setup and teardown. When True,
302             logging is enabled, otherwise logging is disabled.
303         :type node: dict
304         :type path: str
305         :type headers: dict
306         :type payload: dict, bytes, or file-like object
307         :type json: str
308         :type timeout: float or tuple
309         :type enable_logging: bool
310         :returns: Status code and content of response.
311         :rtype: tuple
312         """
313         return HTTPRequest._http_request('POST', node, path,
314                                          enable_logging=enable_logging,
315                                          headers=headers, data=payload,
316                                          json=json, timeout=timeout)
317
318     @staticmethod
319     @keyword(name="HTTP Delete")
320     def delete(node, path, timeout=15):
321         """Sends a DELETE request and returns the response and status code.
322
323         :param node: Honeycomb node.
324         :param path: URL path, e.g. /index.html.
325         :param timeout: How long to wait for the server to send data before
326             giving up, as a float, or a (connect timeout, read timeout) tuple.
327         :type node: dict
328         :type path: str
329         :type timeout: float or tuple
330         :returns: Status code and content of response.
331         :rtype: tuple
332         """
333         return HTTPRequest._http_request('DELETE', node, path, timeout=timeout)