# vim: tabstop=4 shiftwidth=4 softtabstop=4 # 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. import logging import requests import six from keystoneclient import exceptions from keystoneclient.openstack.common import jsonutils USER_AGENT = 'python-keystoneclient' _logger = logging.getLogger(__name__) def request(url, method='GET', **kwargs): return Session().request(url, method=method, **kwargs) class Session(object): user_agent = None def __init__(self, session=None, original_ip=None, verify=True, cert=None, timeout=None, user_agent=None): """Maintains client communication state and common functionality. As much as possible the parameters to this class reflect and are passed directly to the requests library. :param string original_ip: The original IP of the requesting user which will be sent to identity service in a 'Forwarded' header. (optional) :param verify: The verification arguments to pass to requests. These are of the same form as requests expects, so True or False to verify (or not) against system certificates or a path to a bundle or CA certs to check against. (optional, defaults to True) :param cert: A client certificate to pass to requests. These are of the same form as requests expects. Either a single filename containing both the certificate and key or a tuple containing the path to the certificate then a path to the key. (optional) :param float timeout: A timeout to pass to requests. This should be a numerical value indicating some amount (or fraction) of seconds or 0 for no timeout. (optional, defaults to 0) :param string user_agent: A User-Agent header string to use for the request. If not provided a default is used. (optional, defaults to 'python-keystoneclient') """ if not session: session = requests.Session() self.session = session self.original_ip = original_ip self.verify = verify self.cert = cert self.timeout = None if timeout is not None: self.timeout = float(timeout) # don't override the class variable if none provided if user_agent is not None: self.user_agent = user_agent def request(self, url, method, json=None, original_ip=None, user_agent=None, **kwargs): """Send an HTTP request with the specified characteristics. Wrapper around `requests.Session.request` to handle tasks such as setting headers, JSON encoding/decoding, and error handling. Arguments that are not handled are passed through to the requests library. :param string url: Fully qualified URL of HTTP request :param string method: The http method to use. (eg. 'GET', 'POST') :param string original_ip: Mark this request as forwarded for this ip. (optional) :param dict headers: Headers to be included in the request. (optional) :param kwargs: any other parameter that can be passed to requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument :param json: Some data to be represented as JSON. (optional) :param string user_agent: A user_agent to use for the request. If present will override one present in headers. (optional) :raises exceptions.ClientException: For connection failure, or to indicate an error response code. :returns: The response to the request. """ headers = kwargs.setdefault('headers', dict()) if self.cert: kwargs.setdefault('cert', self.cert) if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) if user_agent: headers['User-Agent'] = user_agent elif self.user_agent: user_agent = headers.setdefault('User-Agent', self.user_agent) else: user_agent = headers.setdefault('User-Agent', USER_AGENT) if self.original_ip: headers.setdefault('Forwarded', 'for=%s;by=%s' % (self.original_ip, user_agent)) if json is not None: headers['Content-Type'] = 'application/json' kwargs['data'] = jsonutils.dumps(json) kwargs.setdefault('verify', self.verify) string_parts = ['curl -i'] if method: string_parts.extend([' -X ', method]) string_parts.extend([' ', url]) if headers: for header in six.iteritems(headers): string_parts.append(' -H "%s: %s"' % header) _logger.debug('REQ: %s', ''.join(string_parts)) data = kwargs.get('data') if data: _logger.debug('REQ BODY: %s', data) resp = self._send_request(url, method, **kwargs) if resp.status_code >= 400: _logger.debug('Request returned failure status: %s', resp.status_code) raise exceptions.from_response(resp, method, url) return resp def _send_request(self, url, method, **kwargs): try: resp = self.session.request(method, url, **kwargs) except requests.exceptions.SSLError: msg = 'SSL exception connecting to %s' % url raise exceptions.SSLError(msg) except requests.exceptions.Timeout: msg = 'Request to %s timed out' % url raise exceptions.Timeout(msg) except requests.exceptions.ConnectionError: msg = 'Unable to establish connection to %s' % url raise exceptions.ConnectionError(msg) _logger.debug('RESP: [%s] %s\nRESP BODY: %s\n', resp.status_code, resp.headers, resp.text) # NOTE(jamielennox): The requests lib will handle the majority of # redirections. Where it fails is when POSTs are redirected which # is apparently something handled differently by each browser which # requests forces us to do the most compliant way (which we don't want) # see: https://en.wikipedia.org/wiki/Post/Redirect/Get # Nova and other direct users don't do this. Is it still relevant? if resp.status_code in (301, 302, 305): # Redirected. Reissue the request to the new location. return self._send_request(resp.headers['location'], method, **kwargs) return resp def head(self, url, **kwargs): return self.request(url, 'HEAD', **kwargs) def get(self, url, **kwargs): return self.request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self.request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self.request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self.request(url, 'DELETE', **kwargs) def patch(self, url, **kwargs): return self.request(url, 'PATCH', **kwargs)