Added all common modules in conductor directory
[optf/has.git] / conductor / conductor / common / rest.py
1 #
2 # -------------------------------------------------------------------------
3 #   Copyright (c) 2015-2017 AT&T Intellectual Property
4 #
5 #   Licensed under the Apache License, Version 2.0 (the "License");
6 #   you may not use this file except in compliance with the License.
7 #   You may obtain 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,
13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #   See the License for the specific language governing permissions and
15 #   limitations under the License.
16 #
17 # -------------------------------------------------------------------------
18 #
19
20 """REST Helper"""
21
22 import json
23 from os import path
24
25 from oslo_config import cfg
26 from oslo_log import log
27 import requests
28 from requests.auth import HTTPBasicAuth
29 from six.moves.urllib import parse
30
31 from conductor.i18n import _LE, _LW  # pylint: disable=W0212
32
33 LOG = log.getLogger(__name__)
34
35 CONF = cfg.CONF
36
37
38 class RESTException(IOError):
39     """Basic exception for errors raised by REST"""
40
41
42 class CertificateFileNotFoundException(RESTException, ValueError):
43     """Certificate file was not found"""
44
45
46 class MissingURLNetlocException(RESTException, ValueError):
47     """URL is missing a host/port"""
48
49
50 class ProhibitedURLSchemeException(RESTException, ValueError):
51     """URL is using a prohibited scheme"""
52
53
54 class REST(object):
55     """Helper class for REST operations."""
56
57     server_url = None
58     timeout = None
59
60     # Why the funny looking connect/read timeouts? Here, read this:
61     # http://docs.python-requests.org/en/master/user/advanced/#timeouts
62
63     def __init__(self, server_url, retries=3, connect_timeout=3.05,
64                  read_timeout=12.05, username=None, password=None,
65                  cert_file=None, cert_key_file=None, ca_bundle_file=None,
66                  log_debug=False):
67         """Initializer."""
68         parsed = parse.urlparse(server_url, 'http')
69         if parsed.scheme not in ('http', 'https'):
70             raise ProhibitedURLSchemeException
71         if not parsed.netloc:
72             raise MissingURLNetlocException
73
74         for file_path in (cert_file, cert_key_file, ca_bundle_file):
75             if file_path and not path.exists(file_path):
76                 raise CertificateFileNotFoundException
77
78         self.server_url = server_url.rstrip('/')
79         self.retries = int(retries)
80         self.timeout = (float(connect_timeout), float(read_timeout))
81         self.log_debug = log_debug
82         self.username = username
83         self.password = password
84         self.cert = cert_file
85         self.key = cert_key_file
86         self.verify = ca_bundle_file
87
88         # FIXME(jdandrea): Require a CA bundle; do not suppress warnings.
89         # This is here due to an A&AI's cert/server name mismatch.
90         # Permitting this defeats the purpose of using SSL/TLS.
91         if self.verify == "":
92             requests.packages.urllib3.disable_warnings()
93             self.verify = False
94
95         # Use connection pooling, kthx.
96         # http://docs.python-requests.org/en/master/user/advanced/
97         self.session = requests.Session()
98
99     def request(self, method='get', content_type='application/json',
100                 path='', headers=None, data=None):
101         """Performs HTTP request. Returns a requests.Response object."""
102         if method not in ('post', 'get', 'put', 'delete'):
103             method = 'get'
104         method_fn = getattr(self.session, method)
105
106         full_headers = {
107             'Accept': content_type,
108             'Content-Type': content_type,
109         }
110         if headers:
111             full_headers.update(headers)
112         full_url = '{}/{}'.format(self.server_url, path.lstrip('/'))
113
114         # Prepare the request args
115         try:
116             data_str = json.dumps(data) if data else None
117         except (TypeError, ValueError):
118             data_str = data
119         kwargs = {
120             'data': data_str,
121             'headers': full_headers,
122             'timeout': self.timeout,
123             'cert': (self.cert, self.key),
124             'verify': self.verify,
125             'stream': False,
126         }
127         if self.username or self.password:
128             LOG.debug("Using HTTPBasicAuth")
129             kwargs['auth'] = HTTPBasicAuth(self.username, self.password)
130         if self.cert and self.key:
131             LOG.debug("Using SSL/TLS Certificate/Key")
132
133         if self.log_debug:
134             LOG.debug("Request: {} {}".format(method.upper(), full_url))
135             if data:
136                 LOG.debug("Request Body: {}".format(json.dumps(data)))
137         response = None
138         for attempt in range(self.retries):
139             if attempt > 0:
140                 LOG.warn(_LW("Retry #{}/{}").format(
141                     attempt + 1, self.retries))
142
143             try:
144                 response = method_fn(full_url, **kwargs)
145
146                 # We shouldn't have to do this since stream is set to False,
147                 # but we're gonna anyway. See "Body Content Workflow" here:
148                 # http://docs.python-requests.org/en/master/user/advanced/
149                 response.close()
150
151                 if not response.ok:
152                     LOG.warn("Response Status: {} {}".format(
153                         response.status_code, response.reason))
154                 if self.log_debug and response.text:
155                     try:
156                         response_dict = json.loads(response.text)
157                         LOG.debug("Response JSON: {}".format(
158                             json.dumps(response_dict)))
159                     except ValueError:
160                         LOG.debug("Response Body: {}".format(response.text))
161                 if response.ok:
162                     break
163             except requests.exceptions.RequestException as err:
164                 LOG.error("Exception: %s", err.message)
165
166         # Response.__bool__ returns false if status is not ok. Ruh roh!
167         # That means we must check the object type vs treating it as a bool.
168         # More info: https://github.com/kennethreitz/requests/issues/2002
169         if isinstance(response, requests.models.Response) and not response.ok:
170             LOG.error(_LE("Status {} {} after {} retries for URL: {}").format(
171                 response.status_code, response.reason, self.retries, full_url))
172         return response