HC Test: support testing using ipv6 management interface and https
[csit.git] / resources / libraries / python / HTTPRequest.py
1 # Copyright (c) 2016 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.packages.urllib3 import disable_warnings
31 from requests.packages.urllib3.exceptions import InsecureRequestWarning
32 from requests.auth import HTTPBasicAuth
33
34
35 @unique
36 class HTTPCodes(IntEnum):
37     """HTTP status codes"""
38     OK = 200
39     ACCEPTED = 201
40     UNAUTHORIZED = 401
41     FORBIDDEN = 403
42     NOT_FOUND = 404
43     CONFLICT = 409
44     INTERNAL_SERVER_ERROR = 500
45     SERVICE_UNAVAILABLE = 503
46
47
48 class HTTPRequestError(Exception):
49     """Exception raised by HTTPRequest objects.
50
51     When raising this exception, put this information to the message in this
52     order:
53     - short description of the encountered problem,
54     - relevant messages if there are any collected, e.g., from caught
55       exception,
56     - relevant data if there are any collected.
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.error(self._msg)
79             logger.debug(self._details)
80
81         # suppress warnings about disabled SSL verification
82         disable_warnings(InsecureRequestWarning)
83
84     def __repr__(self):
85         return repr(self._msg)
86
87     def __str__(self):
88         return str(self._msg)
89
90
91 class HTTPRequest(object):
92     """A class implementing HTTP requests GET, PUT, POST and DELETE used in
93     communication with Honeycomb.
94
95     The communication with Honeycomb and processing of all exceptions is done in
96     the method _http_request which uses requests.request to send requests and
97     receive responses. The received status code and content of response are
98     logged on the debug level.
99     All possible exceptions raised by requests.request are also processed there.
100
101     The other methods (get, put, post and delete) use _http_request to send
102     corresponding request.
103
104     These methods must not be used as keywords in tests. Use keywords
105     implemented in the module HoneycombAPIKeywords instead.
106     """
107
108     def __init__(self):
109         pass
110
111     @staticmethod
112     def create_full_url(ip_addr, port, path):
113         """Creates full url including host, port, and path to data.
114
115         :param ip_addr: Server IP.
116         :param port: Communication port.
117         :param path: Path to data.
118         :type ip_addr: str
119         :type port: str or int
120         :type path: str
121         :return: Full url.
122         :rtype: str
123         """
124
125         try:
126             IPv6Address(unicode(ip_addr))
127             # IPv6 address must be in brackets
128             ip_addr = "[{0}]".format(ip_addr)
129         except (AttributeError, AddressValueError):
130             pass
131
132         return "https://{ip}:{port}{path}".format(ip=ip_addr, port=port,
133                                                   path=path)
134
135     @staticmethod
136     def _http_request(method, node, path, enable_logging=True, **kwargs):
137         """Sends specified HTTP request and returns status code and response
138         content.
139
140         :param method: The method to be performed on the resource identified by
141         the given request URI.
142         :param node: Honeycomb node.
143         :param path: URL path, e.g. /index.html.
144         :param enable_logging: Used to suppress errors when checking Honeycomb
145         state during suite setup and teardown.
146         :param kwargs: Named parameters accepted by request.request:
147             params -- (optional) Dictionary or bytes to be sent in the query
148             string for the Request.
149             data -- (optional) Dictionary, bytes, or file-like object to
150             send in the body of the Request.
151             json -- (optional) json data to send in the body of the Request.
152             headers -- (optional) Dictionary of HTTP Headers to send with
153             the Request.
154             cookies -- (optional) Dict or CookieJar object to send with the
155             Request.
156             files -- (optional) Dictionary of 'name': file-like-objects
157             (or {'name': ('filename', fileobj)}) for multipart encoding upload.
158             timeout (float or tuple) -- (optional) How long to wait for the
159             server to send data before giving up, as a float, or a (connect
160             timeout, read timeout) tuple.
161             allow_redirects (bool) -- (optional) Boolean. Set to True if POST/
162             PUT/DELETE redirect following is allowed.
163             proxies -- (optional) Dictionary mapping protocol to the URL of
164             the proxy.
165             verify -- (optional) whether the SSL cert will be verified.
166             A CA_BUNDLE path can also be provided. Defaults to True.
167             stream -- (optional) if False, the response content will be
168             immediately downloaded.
169             cert -- (optional) if String, path to ssl client cert file (.pem).
170             If Tuple, ('cert', 'key') pair.
171         :type method: str
172         :type node: dict
173         :type path: str
174         :type enable_logging: bool
175         :type kwargs: dict
176         :return: Status code and content of response.
177         :rtype: tuple
178         :raises HTTPRequestError: If
179         1. it is not possible to connect,
180         2. invalid HTTP response comes from server,
181         3. request exceeded the configured number of maximum re-directions,
182         4. request timed out,
183         5. there is any other unexpected HTTP request exception.
184         """
185         timeout = kwargs["timeout"]
186
187         if BuiltIn().get_variable_value("${use_odl_client}"):
188             # TODO: node["honeycomb"]["odl_port"]
189             port = 8181
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=10, 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, logging is enabled,
246         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         :return: 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=10):
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         :return: 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=10,
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, logging is enabled,
302         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         :return: 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=10):
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         :return: Status code and content of response.
331         :rtype: tuple
332         """
333         return HTTPRequest._http_request('DELETE', node, path, timeout=timeout)