aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/HTTPRequest.py
blob: 0f650a89a193d6920571cf68432f153059918537 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# Copyright (c) 2018 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Implementation of HTTP requests GET, PUT, POST and DELETE used in
communication with Honeycomb.

The HTTP requests are implemented in the class HTTPRequest which uses
requests.request.
"""

from ipaddress import IPv6Address, AddressValueError
from enum import IntEnum, unique

from robot.api.deco import keyword
from robot.api import logger
from robot.libraries.BuiltIn import BuiltIn

from requests import request, RequestException, Timeout, TooManyRedirects, \
    HTTPError, ConnectionError
from requests.auth import HTTPBasicAuth


@unique
class HTTPCodes(IntEnum):
    """HTTP status codes"""
    OK = 200  # HTTP standard code name.  # pylint: disable=invalid-name
    ACCEPTED = 201
    UNAUTHORIZED = 401
    FORBIDDEN = 403
    NOT_FOUND = 404
    CONFLICT = 409
    INTERNAL_SERVER_ERROR = 500
    SERVICE_UNAVAILABLE = 503


class HTTPRequestError(Exception):
    """Exception raised by HTTPRequest objects.

    When raising this exception, put this information to the message in this
    order:

     - short description of the encountered problem,
     - relevant messages if there are any collected, e.g., from caught
       exception,
     - relevant data if there are any collected.

    The logging is performed on two levels: 1. error - short description of the
    problem; 2. debug - detailed information.
    """

    def __init__(self, msg, details='', enable_logging=True):
        """Sets the exception message and enables / disables logging.

        It is not wanted to log errors when using these keywords together
        with keywords like "Wait until keyword succeeds". So you can disable
        logging by setting enable_logging to False.

        :param msg: Message to be displayed and logged.
        :param enable_logging: When True, logging is enabled, otherwise
            logging is disabled.
        :type msg: str
        :type enable_logging: bool
        """
        super(HTTPRequestError, self).__init__()
        self._msg = "{0}: {1}".format(self.__class__.__name__, msg)
        self._details = details
        if enable_logging:
            logger.info(self._msg)
            logger.debug(self._details)

    def __repr__(self):
        return repr(self._msg)

    def __str__(self):
        return str(self._msg)


class HTTPRequest(object):
    """A class implementing HTTP requests GET, PUT, POST and DELETE used in
    communication with Honeycomb.

    The communication with Honeycomb and processing of all exceptions is done in
    the method _http_request which uses requests.request to send requests and
    receive responses. The received status code and content of response are
    logged on the debug level.
    All possible exceptions raised by requests.request are also processed there.

    The other methods (get, put, post and delete) use _http_request to send
    corresponding request.

    These methods must not be used as keywords in tests. Use keywords
    implemented in the module HoneycombAPIKeywords instead.
    """

    def __init__(self):
        pass

    @staticmethod
    def create_full_url(ip_addr, port, path):
        """Creates full url including host, port, and path to data.

        :param ip_addr: Server IP.
        :param port: Communication port.
        :param path: Path to data.
        :type ip_addr: str
        :type port: str or int
        :type path: str
        :returns: Full url.
        :rtype: str
        """

        try:
            IPv6Address(unicode(ip_addr))
            # IPv6 address must be in brackets
            ip_addr = "[{0}]".format(ip_addr)
        except (AttributeError, AddressValueError):
            pass

        return "http://{ip}:{port}{path}".format(ip=ip_addr, port=port,
                                                 path=path)

    @staticmethod
    def _http_request(method, node, path, enable_logging=True, **kwargs):
        """Sends specified HTTP request and returns status code and response
        content.

        :param method: The method to be performed on the resource identified by
        the given request URI.
        :param node: Honeycomb node.
        :param path: URL path, e.g. /index.html.
        :param enable_logging: Used to suppress errors when checking Honeycomb
        state during suite setup and teardown.
        :param kwargs: Named parameters accepted by request.request:
            params -- (optional) Dictionary or bytes to be sent in the query
            string for the Request.
            data -- (optional) Dictionary, bytes, or file-like object to
            send in the body of the Request.
            json -- (optional) json data to send in the body of the Request.
            headers -- (optional) Dictionary of HTTP Headers to send with
            the Request.
            cookies -- (optional) Dict or CookieJar object to send with the
            Request.
            files -- (optional) Dictionary of 'name': file-like-objects
            (or {'name': ('filename', fileobj)}) for multipart encoding upload.
            timeout (float or tuple) -- (optional) How long to wait for the
            server to send data before giving up, as a float, or a (connect
            timeout, read timeout) tuple.
            allow_redirects (bool) -- (optional) Boolean. Set to True if POST/
            PUT/DELETE redirect following is allowed.
            proxies -- (optional) Dictionary mapping protocol to the URL of
            the proxy.
            verify -- (optional) whether the SSL cert will be verified.
            A CA_BUNDLE path can also be provided. Defaults to True.
            stream -- (optional) if False, the response content will be
            immediately downloaded.
            cert -- (optional) if String, path to ssl client cert file (.pem).
            If Tuple, ('cert', 'key') pair.
        :type method: str
        :type node: dict
        :type path: str
        :type enable_logging: bool
        :type kwargs: dict
        :returns: Status code and content of response.
        :rtype: tuple
        :raises HTTPRequestError: If
        1. it is not possible to connect,
        2. invalid HTTP response comes from server,
        3. request exceeded the configured number of maximum re-directions,
        4. request timed out,
        5. there is any other unexpected HTTP request exception.
        """
        timeout = kwargs["timeout"]

        use_odl = BuiltIn().get_variable_value("${use_odl_client}")

        if use_odl:
            port = 8181
            # Using default ODL Restconf port
            # TODO: add node["honeycomb"]["odl_port"] to topology, use it here
            odl_url_part = "/network-topology:network-topology/topology/" \
                           "topology-netconf/node/vpp/yang-ext:mount"
        else:
            port = node["honeycomb"]["port"]
            odl_url_part = ""

        try:
            path = path.format(odl_url_part=odl_url_part)
        except KeyError:
            pass

        url = HTTPRequest.create_full_url(node['host'],
                                          port,
                                          path)
        try:
            auth = HTTPBasicAuth(node['honeycomb']['user'],
                                 node['honeycomb']['passwd'])
            rsp = request(method, url, auth=auth, verify=False, **kwargs)

            logger.debug("Status code: {0}".format(rsp.status_code))
            logger.debug("Response: {0}".format(rsp.content))

            return rsp.status_code, rsp.content

        except ConnectionError as err:
            # Switching the logging on / off is needed only for
            # "requests.ConnectionError"
            raise HTTPRequestError("Not possible to connect to {0}:{1}.".
                                   format(node['host'],
                                          node['honeycomb']['port']),
                                   repr(err), enable_logging=enable_logging)
        except HTTPError as err:
            raise HTTPRequestError("Invalid HTTP response from {0}.".
                                   format(node['host']), repr(err))
        except TooManyRedirects as err:
            raise HTTPRequestError("Request exceeded the configured number "
                                   "of maximum re-directions.", repr(err))
        except Timeout as err:
            raise HTTPRequestError("Request timed out. Timeout is set to {0}.".
                                   format(timeout), repr(err))
        except RequestException as err:
            raise HTTPRequestError("Unexpected HTTP request exception.",
                                   repr(err))

    @staticmethod
    @keyword(name="HTTP Get")
    def get(node, path, headers=None, timeout=15, enable_logging=True):
        """Sends a GET request and returns the response and status code.

        :param node: Honeycomb node.
        :param path: URL path, e.g. /index.html.
        :param headers: Dictionary of HTTP Headers to send with the Request.
        :param timeout: How long to wait for the server to send data before
            giving up, as a float, or a (connect timeout, read timeout) tuple.
        :param enable_logging: Used to suppress errors when checking Honeycomb
            state during suite setup and teardown. When True,
            logging is enabled, otherwise logging is disabled.
        :type node: dict
        :type path: str
        :type headers: dict
        :type timeout: float or tuple
        :type enable_logging: bool
        :returns: Status code and content of response.
        :rtype: tuple
        """

        return HTTPRequest._http_request('GET', node, path,
                                         enable_logging=enable_logging,
                                         headers=headers, timeout=timeout)

    @staticmethod
    @keyword(name="HTTP Put")
    def put(node, path, headers=None, payload=None, json=None, timeout=15):
        """Sends a PUT request and returns the response and status code.

        :param node: Honeycomb node.
        :param path: URL path, e.g. /index.html.
        :param headers: Dictionary of HTTP Headers to send with the Request.
        :param payload: Dictionary, bytes, or file-like object to send in
            the body of the Request.
        :param json: JSON formatted string to send in the body of the Request.
        :param timeout: How long to wait for the server to send data before
            giving up, as a float, or a (connect timeout, read timeout) tuple.
        :type node: dict
        :type path: str
        :type headers: dict
        :type payload: dict, bytes, or file-like object
        :type json: str
        :type timeout: float or tuple
        :returns: Status code and content of response.
        :rtype: tuple
        """
        return HTTPRequest._http_request('PUT', node, path, headers=headers,
                                         data=payload, json=json,
                                         timeout=timeout)

    @staticmethod
    @keyword(name="HTTP Post")
    def post(node, path, headers=None, payload=None, json=None, timeout=15,
             enable_logging=True):
        """Sends a POST request and returns the response and status code.

        :param node: Honeycomb node.
        :param path: URL path, e.g. /index.html.
        :param headers: Dictionary of HTTP Headers to send with the Request.
        :param payload: Dictionary, bytes, or file-like object to send in
            the body of the Request.
        :param json: JSON formatted string to send in the body of the Request.
        :param timeout: How long to wait for the server to send data before
            giving up, as a float, or a (connect timeout, read timeout) tuple.
        :param enable_logging: Used to suppress errors when checking ODL
            state during suite setup and teardown. When True,
            logging is enabled, otherwise logging is disabled.
        :type node: dict
        :type path: str
        :type headers: dict
        :type payload: dict, bytes, or file-like object
        :type json: str
        :type timeout: float or tuple
        :type enable_logging: bool
        :returns: Status code and content of response.
        :rtype: tuple
        """
        return HTTPRequest._http_request('POST', node, path,
                                         enable_logging=enable_logging,
                                         headers=headers, data=payload,
                                         json=json, timeout=timeout)

    @staticmethod
    @keyword(name="HTTP Delete")
    def delete(node, path, timeout=15):
        """Sends a DELETE request and returns the response and status code.

        :param node: Honeycomb node.
        :param path: URL path, e.g. /index.html.
        :param timeout: How long to wait for the server to send data before
            giving up, as a float, or a (connect timeout, read timeout) tuple.
        :type node: dict
        :type path: str
        :type timeout: float or tuple
        :returns: Status code and content of response.
        :rtype: tuple
        """
        return HTTPRequest._http_request('DELETE', node, path, timeout=timeout)