diff --git a/glareclient/client.py b/glareclient/client.py index 1ca0c6e..4987b8d 100644 --- a/glareclient/client.py +++ b/glareclient/client.py @@ -13,48 +13,24 @@ # License for the specific language governing permissions and limitations # under the License. -import warnings - from glareclient.common import utils -def Client(version=None, endpoint=None, session=None, *args, **kwargs): +def Client(version='1', endpoint=None, session=None, *args, **kwargs): """Client for the Glare Artifact Repository. Generic client for the Glare Artifact Repository. See version classes for specific details. - :param string version: The version of API to use. :param session: A keystoneauth1 session that should be used for transport. :type session: keystoneauth1.session.Session """ - # FIXME(jamielennox): Add a deprecation warning if no session is passed. - # Leaving it as an option until we can ensure nothing break when we switch. - if session: - if endpoint: - kwargs.setdefault('endpoint_override', endpoint) - if not version: - __, version = utils.strip_version(endpoint) + if endpoint is not None: + kwargs.setdefault('endpoint_override', endpoint) - if not version: - msg = ("You must provide a client version when using session") - raise RuntimeError(msg) - - else: - if version is not None: - warnings.warn(("`version` keyword is being deprecated. Please pass" - " the version as part of the URL. " - "http://$HOST:$PORT/v$VERSION_NUMBER"), - DeprecationWarning) - - endpoint, url_version = utils.strip_version(endpoint) - version = version or url_version - - if not version: - msg = ("Please provide either the version or an url with the form " - "http://$HOST:$PORT/v$VERSION_NUMBER") - raise RuntimeError(msg) + if version is None: + raise RuntimeError("You must provide a client version") module = utils.import_versioned_module(int(version), 'client') client_class = getattr(module, 'Client') diff --git a/glareclient/common/http.py b/glareclient/common/http.py index eafb6bb..8cc1458 100644 --- a/glareclient/common/http.py +++ b/glareclient/common/http.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack Foundation +# Copyright 2012 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,102 +14,79 @@ # under the License. import copy -import logging +import hashlib +import os import socket from keystoneauth1 import adapter -from keystoneauth1 import exceptions as ksa_exc -from oslo_utils import importutils -from oslo_utils import netutils +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import encodeutils import requests import six -import warnings +from six.moves import urllib -try: - import json -except ImportError: - import simplejson as json - -from oslo_utils import encodeutils - -from glareclient.common import utils -from glareclient import exc - -osprofiler_web = importutils.try_import("osprofiler.web") +from glareclient._i18n import _ +from glareclient.common import exceptions as exc LOG = logging.getLogger(__name__) USER_AGENT = 'python-glareclient' CHUNKSIZE = 1024 * 64 # 64kB -def encode_headers(headers): - """Encodes headers. - - Note: This should be used right before - sending anything out. - - :param headers: Headers to encode - :returns: Dictionary with encoded headers' - names and values - """ - return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v)) - for h, v in six.iteritems(headers) if v is not None) +def get_system_ca_file(): + """Return path to system default CA file.""" + # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, + # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca + ca_path = ['/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/etc/ssl/cert.pem', + '/System/Library/OpenSSL/certs/cacert.pem', + requests.certs.where()] + for ca in ca_path: + LOG.debug("Looking for ca file %s", ca) + if os.path.exists(ca): + LOG.debug("Using ca file %s", ca) + return ca + LOG.warning("System ca file could not be found.") -class _BaseHTTPClient(object): +def _chunk_body(body): + chunk = body + while chunk: + chunk = body.read(CHUNKSIZE) + if not chunk: + break + yield chunk - @staticmethod - def _chunk_body(body): - chunk = body - while chunk: - chunk = body.read(CHUNKSIZE) - if not chunk: - break - yield chunk - def _set_common_request_kwargs(self, headers, kwargs): - """Handle the common parameters used to send the request.""" +def _set_request_params(kwargs_params): + data = kwargs_params.pop('data', None) + params = copy.deepcopy(kwargs_params) + headers = params.get('headers', {}) + content_type = headers.get('Content-Type', 'application/json') - # Default Content-Type is octet-stream - content_type = headers.get('Content-Type', 'application/json') - data = kwargs.pop("data", None) - if data is not None and not isinstance(data, six.string_types): - try: - data = json.dumps(data) - except TypeError: - data = self._chunk_body(data) - content_type = 'application/octet-stream' + if data is not None and not isinstance(data, six.string_types): + if content_type.startswith('application/json'): + data = jsonutils.dumps(data) + if content_type == 'application/octet-stream': + data = _chunk_body(data) - headers['Content-Type'] = content_type - kwargs['stream'] = content_type == 'application/octet-stream' - return data + params['data'] = data + headers.update({'Content-Type': content_type}) + params['headers'] = headers + params['stream'] = content_type == 'application/octet-stream' - def _handle_response(self, resp): - # log request-id for each api cal - request_id = resp.headers.get('x-openstack-request-id') - if request_id: - LOG.debug('%(method)s call to glare-api for ' - '%(url)s used request id ' - '%(response_request_id)s', - {'method': resp.request.method, - 'url': resp.url, - 'response_request_id': request_id}) + return params - if not resp.ok: - LOG.debug("Request returned failure status %s.", resp.status_code) - raise exc.from_response(resp, resp.content) - elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and - resp.request.path_url != '/versions'): - # NOTE(flaper87): Eventually, we'll remove the check on `versions` - # which is a bug (1491350) on the server. - raise exc.from_response(resp) +def _handle_response(resp): content_type = resp.headers.get('Content-Type') - if not content_type: body_iter = six.StringIO(resp.text) try: - body_iter = json.loads(''.join([c for c in body_iter])) + body_iter = jsonutils.loads(''.join([c for c in body_iter])) except ValueError: body_iter = None elif content_type.startswith('application/json'): @@ -122,172 +99,6 @@ class _BaseHTTPClient(object): return resp, body_iter -class HTTPClient(_BaseHTTPClient): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.identity_headers = kwargs.get('identity_headers') - self.auth_token = kwargs.get('token') - self.language_header = kwargs.get('language_header') - self.last_request_id = None - if self.identity_headers: - if self.identity_headers.get('X-Auth-Token'): - self.auth_token = self.identity_headers.get('X-Auth-Token') - del self.identity_headers['X-Auth-Token'] - - self.session = requests.Session() - self.session.headers["User-Agent"] = USER_AGENT - - if self.language_header: - self.session.headers["Accept-Language"] = self.language_header - - self.timeout = float(kwargs.get('timeout', 600)) - - if self.endpoint.startswith("https"): - compression = kwargs.get('ssl_compression', True) - - if compression is False: - # Note: This is not seen by default. (python must be - # run with -Wd) - warnings.warn('The "ssl_compression" argument has been ' - 'deprecated.', DeprecationWarning) - - if kwargs.get('insecure', False) is True: - self.session.verify = False - else: - if kwargs.get('cacert', None) is not '': - self.session.verify = kwargs.get('cacert', True) - - self.session.cert = (kwargs.get('cert_file'), - kwargs.get('key_file')) - - @staticmethod - def parse_endpoint(endpoint): - return netutils.urlsplit(endpoint) - - def log_curl_request(self, method, url, headers, data, kwargs): - curl = ['curl -g -i -X %s' % method] - - headers = copy.deepcopy(headers) - headers.update(self.session.headers) - - for (key, value) in six.iteritems(headers): - header = '-H \'%s: %s\'' % utils.safe_header(key, value) - curl.append(header) - - if not self.session.verify: - curl.append('-k') - else: - if isinstance(self.session.verify, six.string_types): - curl.append(' --cacert %s' % self.session.verify) - - if self.session.cert: - curl.append(' --cert %s --key %s' % self.session.cert) - - if data and isinstance(data, six.string_types): - curl.append('-d \'%s\'' % data) - - curl.append(url) - - msg = ' '.join([encodeutils.safe_decode(item, errors='ignore') - for item in curl]) - LOG.debug(msg) - - @staticmethod - def log_http_response(resp): - status = (resp.raw.version / 10.0, resp.status_code, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - headers = resp.headers.items() - dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers]) - dump.append('') - content_type = resp.headers.get('Content-Type') - - if content_type != 'application/octet-stream': - dump.extend([resp.text, '']) - LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore') - for x in dump])) - - def _request(self, method, url, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around httplib.HTTP(S)Connection.request to handle tasks such - as setting headers and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - headers = copy.deepcopy(kwargs.pop('headers', {})) - - if self.identity_headers: - for k, v in six.iteritems(self.identity_headers): - headers.setdefault(k, v) - - data = self._set_common_request_kwargs(headers, kwargs) - - # add identity header to the request - if not headers.get('X-Auth-Token'): - headers['X-Auth-Token'] = self.auth_token - - if osprofiler_web: - headers.update(osprofiler_web.get_trace_id_headers()) - - # Note(flaper87): Before letting headers / url fly, - # they should be encoded otherwise httplib will - # complain. - headers = encode_headers(headers) - - if self.endpoint.endswith("/") or url.startswith("/"): - conn_url = "%s%s" % (self.endpoint, url) - else: - conn_url = "%s/%s" % (self.endpoint, url) - self.log_curl_request(method, conn_url, headers, data, kwargs) - - try: - resp = self.session.request(method, - conn_url, - data=data, - headers=headers, - **kwargs) - except requests.exceptions.Timeout as e: - message = ("Error communicating with %(url)s: %(e)s" % - dict(url=conn_url, e=e)) - raise exc.InvalidEndpoint(message=message) - except requests.exceptions.ConnectionError as e: - message = ("Error finding address for %(url)s: %(e)s" % - dict(url=conn_url, e=e)) - raise exc.CommunicationError(message=message) - except socket.gaierror as e: - message = "Error finding address for %s: %s" % ( - self.endpoint_hostname, e) - raise exc.InvalidEndpoint(message=message) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - message = ("Error communicating with %(endpoint)s %(e)s" % - {'endpoint': endpoint, 'e': e}) - raise exc.CommunicationError(message=message) - - self.last_request_id = resp.headers.get('x-openstack-request-id') - resp, body_iter = self._handle_response(resp) - self.log_http_response(resp) - return resp, body_iter - - def head(self, url, **kwargs): - return self._request('HEAD', url, **kwargs) - - def get(self, url, **kwargs): - return self._request('GET', url, **kwargs) - - def post(self, url, **kwargs): - return self._request('POST', url, **kwargs) - - def put(self, url, **kwargs): - return self._request('PUT', url, **kwargs) - - def patch(self, url, **kwargs): - return self._request('PATCH', url, **kwargs) - - def delete(self, url, **kwargs): - return self._request('DELETE', url, **kwargs) - - def _close_after_stream(response, chunk_size): """Iterate over the content and ensure the response is closed after.""" # Yield each chunk in the response body @@ -299,45 +110,278 @@ def _close_after_stream(response, chunk_size): response.close() -class SessionClient(adapter.Adapter, _BaseHTTPClient): +class HTTPClient(object): - def __init__(self, session, **kwargs): - kwargs.setdefault('user_agent', USER_AGENT) - kwargs.setdefault('service_type', 'artifact') - self.last_request_id = None - super(SessionClient, self).__init__(session, **kwargs) + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.auth_url = kwargs.get('auth_url') + self.auth_token = kwargs.get('token') + self.username = kwargs.get('username') + self.password = kwargs.get('password') + self.region_name = kwargs.get('region_name') + self.include_pass = kwargs.get('include_pass') + self.endpoint_url = endpoint - def request(self, url, method, **kwargs): - headers = encode_headers(kwargs.pop('headers', {})) - kwargs['raise_exc'] = False - data = self._set_common_request_kwargs(headers, kwargs) + self.cert_file = kwargs.get('cert_file') + self.key_file = kwargs.get('key_file') + self.timeout = kwargs.get('timeout') + + self.ssl_connection_params = { + 'cacert': kwargs.get('cacert'), + 'cert_file': kwargs.get('cert_file'), + 'key_file': kwargs.get('key_file'), + 'insecure': kwargs.get('insecure'), + } + + self.verify_cert = None + if urllib.parse.urlparse(endpoint).scheme == "https": + if kwargs.get('insecure'): + self.verify_cert = False + else: + self.verify_cert = kwargs.get('cacert', get_system_ca_file()) + + def _safe_header(self, name, value): + if name in ['X-Auth-Token', 'X-Subject-Token']: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) + + def log_curl_request(self, url, method, kwargs): + curl = ['curl -i -X %s' % method] + + for (key, value) in kwargs['headers'].items(): + header = '-H \'%s: %s\'' % self._safe_header(key, value) + curl.append(header) + + conn_params_fmt = [ + ('key_file', '--key %s'), + ('cert_file', '--cert %s'), + ('cacert', '--cacert %s'), + ] + for (key, fmt) in conn_params_fmt: + value = self.ssl_connection_params.get(key) + if value: + curl.append(fmt % value) + + if self.ssl_connection_params.get('insecure'): + curl.append('-k') + + if 'data' in kwargs: + curl.append('-d \'%s\'' % kwargs['data']) + + curl.append('%s%s' % (self.endpoint, url)) + LOG.debug(' '.join(curl)) + + @staticmethod + def log_http_response(resp): + status = (resp.raw.version / 10.0, resp.status_code, resp.reason) + dump = ['\nHTTP/%.1f %s %s' % status] + dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) + dump.append('') + if resp.content: + content = resp.content + if isinstance(content, six.binary_type): + try: + content = encodeutils.safe_decode(resp.content) + except UnicodeDecodeError: + pass + else: + dump.extend([content, '']) + LOG.debug('\n'.join(dump)) + + def request(self, url, method, log=True, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around requests.request to handle tasks such + as setting headers and error handling. + """ + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', USER_AGENT) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + else: + kwargs['headers'].update(self.credentials_headers()) + if self.auth_url: + kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) + if self.region_name: + kwargs['headers'].setdefault('X-Region-Name', self.region_name) + + self.log_curl_request(url, method, kwargs) + + if self.cert_file and self.key_file: + kwargs['cert'] = (self.cert_file, self.key_file) + + if self.verify_cert is not None: + kwargs['verify'] = self.verify_cert + + if self.timeout is not None: + kwargs['timeout'] = float(self.timeout) + + # Allow the option not to follow redirects + follow_redirects = kwargs.pop('follow_redirects', True) + + # Since requests does not follow the RFC when doing redirection to sent + # back the same method on a redirect we are simply bypassing it. For + # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says + # that we should follow that URL with the same method as before, + # requests doesn't follow that and send a GET instead for the method. + # Hopefully this could be fixed as they say in a comment in a future + # point version i.e.: 3.x + # See issue: https://github.com/kennethreitz/requests/issues/1704 + allow_redirects = False try: - resp = super(SessionClient, self).request(url, - method, - headers=headers, - data=data, - **kwargs) - except ksa_exc.ConnectTimeout as e: - conn_url = self.get_endpoint(auth=kwargs.get('auth')) - conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) - message = ("Error communicating with %(url)s %(e)s" % - dict(url=conn_url, e=e)) - raise exc.InvalidEndpoint(message=message) - except ksa_exc.ConnectFailure as e: - conn_url = self.get_endpoint(auth=kwargs.get('auth')) - conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) + resp = requests.request( + method, + self.endpoint_url + url, + allow_redirects=allow_redirects, + **kwargs) + except socket.gaierror as e: message = ("Error finding address for %(url)s: %(e)s" % - dict(url=conn_url, e=e)) + {'url': self.endpoint_url + url, 'e': e}) + raise exc.InvalidEndpoint(message=message) + except (socket.error, + socket.timeout, + requests.exceptions.ConnectionError) as e: + endpoint = self.endpoint + message = ("Error communicating with %(endpoint)s %(e)s" % + {'endpoint': endpoint, 'e': e}) raise exc.CommunicationError(message=message) - self.last_request_id = resp.headers.get('x-openstack-request-id') - return self._handle_response(resp) + if log: + self.log_http_response(resp) + + if 'X-Auth-Key' not in kwargs['headers'] and \ + (resp.status_code == 401 or + (resp.status_code == 500 and + "(HTTP 401)" in resp.content)): + raise exc.HTTPUnauthorized("Authentication failed. Please try" + " again.\n%s" + % resp.content) + elif 400 <= resp.status_code < 600: + raise exc.from_response(resp) + elif resp.status_code in (301, 302, 305): + # Redirected. Reissue the request to the new location, + # unless caller specified follow_redirects=False + if follow_redirects: + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp = self.request(path, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp) + + return resp + + def strip_endpoint(self, location): + if location is None: + message = "Location not returned with 302" + raise exc.InvalidEndpoint(message=message) + elif location.startswith(self.endpoint): + return location[len(self.endpoint):] + else: + message = "Prohibited endpoint redirect %s" % location + raise exc.InvalidEndpoint(message=message) + + def credentials_headers(self): + creds = {} + if self.username: + creds['X-Auth-User'] = self.username + if self.password: + creds['X-Auth-Key'] = self.password + return creds + + def json_request(self, url, method, **kwargs): + params = _set_request_params(kwargs) + resp = self.request(url, method, **params) + return _handle_response(resp) + + def json_patch_request(self, url, method='PATCH', **kwargs): + return self.json_request( + url, method, **kwargs) + + def head(self, url, **kwargs): + return self.json_request(url, "HEAD", **kwargs) + + def get(self, url, **kwargs): + return self.json_request(url, "GET", **kwargs) + + def post(self, url, **kwargs): + return self.json_request(url, "POST", **kwargs) + + def put(self, url, **kwargs): + return self.json_request(url, "PUT", **kwargs) + + def delete(self, url, **kwargs): + return self.request(url, "DELETE", **kwargs) + + def patch(self, url, **kwargs): + return self.json_request(url, "PATCH", **kwargs) -def get_http_client(endpoint=None, session=None, **kwargs): +class SessionClient(adapter.LegacyJsonAdapter): + """HTTP client based on Keystone client session.""" + + def request(self, url, method, **kwargs): + params = _set_request_params(kwargs) + redirect = kwargs.get('redirect') + + resp, body = super(SessionClient, self).request( + url, method, + **params) + + if 400 <= resp.status_code < 600: + raise exc.from_response(resp) + elif resp.status_code in (301, 302, 305): + if redirect: + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp = self.request(path, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp) + + if resp.headers.get('Content-Type') == 'application/octet-stream': + body = _close_after_stream(resp, CHUNKSIZE) + return resp, body + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with 302") + raise exc.InvalidEndpoint(message=message) + if (self.endpoint_override is not None and + location.lower().startswith(self.endpoint_override.lower())): + return location[len(self.endpoint_override):] + else: + return location + + +def construct_http_client(*args, **kwargs): + session = kwargs.pop('session', None) + auth = kwargs.pop('auth', None) + endpoint = next(iter(args), None) + if session: - return SessionClient(session, **kwargs) + service_type = kwargs.pop('service_type', None) + endpoint_type = kwargs.pop('endpoint_type', None) + region_name = kwargs.pop('region_name', None) + service_name = kwargs.pop('service_name', None) + parameters = { + 'endpoint_override': endpoint, + 'session': session, + 'auth': auth, + 'interface': endpoint_type, + 'service_type': service_type, + 'region_name': region_name, + 'service_name': service_name, + 'user_agent': 'python-glareclient', + } + parameters.update(kwargs) + return SessionClient(**parameters) elif endpoint: return HTTPClient(endpoint, **kwargs) else: diff --git a/glareclient/common/https.py b/glareclient/common/https.py deleted file mode 100644 index 94bbda6..0000000 --- a/glareclient/common/https.py +++ /dev/null @@ -1,347 +0,0 @@ -# Copyright 2014 Red Hat, Inc -# All Rights Reserved. -# -# 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 socket -import ssl -import struct - -import OpenSSL -from requests import adapters -from requests import compat -try: - from requests.packages.urllib3 import connectionpool - from requests.packages.urllib3 import poolmanager -except ImportError: - from urllib3 import connectionpool - from urllib3 import poolmanager - - -from oslo_utils import encodeutils -import six -# NOTE(jokke): simplified transition to py3, behaves like py2 xrange -from six.moves import range - -try: - from eventlet import patcher - # Handle case where we are running in a monkey patched environment - if patcher.is_monkey_patched('socket'): - from eventlet.green.httplib import HTTPSConnection - from eventlet.green.OpenSSL.SSL import GreenConnection as Connection - else: - raise ImportError -except ImportError: - from OpenSSL import SSL - from six.moves import http_client - HTTPSConnection = http_client.HTTPSConnection - Connection = SSL.Connection - - -from glareclient import exc - - -def verify_callback(host=None): - """Provide wrapper for do_verify_callback. - - We use a partial around the 'real' verify_callback function - so that we can stash the host value without holding a - reference on the VerifiedHTTPSConnection. - """ - def wrapper(connection, x509, errnum, - depth, preverify_ok, host=host): - return do_verify_callback(connection, x509, errnum, - depth, preverify_ok, host=host) - return wrapper - - -def do_verify_callback(connection, x509, errnum, - depth, preverify_ok, host=None): - """Verify the server's SSL certificate. - - This is a standalone function rather than a method to avoid - issues around closing sockets if a reference is held on - a VerifiedHTTPSConnection by the callback function. - """ - if x509.has_expired(): - msg = "SSL Certificate expired on '%s'" % x509.get_notAfter() - raise exc.SSLCertificateError(msg) - - if depth == 0 and preverify_ok: - # We verify that the host matches against the last - # certificate in the chain - return host_matches_cert(host, x509) - else: - # Pass through OpenSSL's default result - return preverify_ok - - -def host_matches_cert(host, x509): - """Verify the certificate identifies the host. - - Verify that the x509 certificate we have received - from 'host' correctly identifies the server we are - connecting to, ie that the certificate's Common Name - or a Subject Alternative Name matches 'host'. - """ - def check_match(name): - # Directly match the name - if name == host: - return True - - # Support single wildcard matching - if name.startswith('*.') and host.find('.') > 0: - if name[2:] == host.split('.', 1)[1]: - return True - - common_name = x509.get_subject().commonName - - # First see if we can match the CN - if check_match(common_name): - return True - # Also try Subject Alternative Names for a match - san_list = None - for i in range(x509.get_extension_count()): - ext = x509.get_extension(i) - if ext.get_short_name() == b'subjectAltName': - san_list = str(ext) - for san in ''.join(san_list.split()).split(','): - if san.startswith('DNS:'): - if check_match(san.split(':', 1)[1]): - return True - - # Server certificate does not match host - msg = ('Host "%s" does not match x509 certificate contents: ' - 'CommonName "%s"' % (host, common_name)) - if san_list is not None: - msg = msg + ', subjectAltName "%s"' % san_list - raise exc.SSLCertificateError(msg) - - -def to_bytes(s): - if isinstance(s, six.string_types): - return six.b(s) - else: - return s - - -class HTTPSAdapter(adapters.HTTPAdapter): - """This adapter will be used just when ssl compression should be disabled. - - The init method overwrites the default https pool by setting - glareclient's one. - """ - def __init__(self, *args, **kwargs): - classes_by_scheme = poolmanager.pool_classes_by_scheme - classes_by_scheme["glare+https"] = HTTPSConnectionPool - super(HTTPSAdapter, self).__init__(*args, **kwargs) - - def request_url(self, request, proxies): - # NOTE(flaper87): Make sure the url is encoded, otherwise - # python's standard httplib will fail with a TypeError. - url = super(HTTPSAdapter, self).request_url(request, proxies) - if six.PY2: - url = encodeutils.safe_encode(url) - return url - - def _create_glare_httpsconnectionpool(self, url): - kw = self.poolmanager.connection_pool_kw - # Parse the url to get the scheme, host, and port - parsed = compat.urlparse(url) - # If there is no port specified, we should use the standard HTTPS port - port = parsed.port or 443 - host = parsed.netloc.rsplit(':', 1)[0] - pool = HTTPSConnectionPool(host, port, **kw) - - with self.poolmanager.pools.lock: - self.poolmanager.pools[(parsed.scheme, host, port)] = pool - - return pool - - def get_connection(self, url, proxies=None): - try: - return super(HTTPSAdapter, self).get_connection(url, proxies) - except KeyError: - # NOTE(sigamvirus24): This works around modifying a module global - # which fixes bug #1396550 - # The scheme is most likely glare+https but check anyway - if not url.startswith('glare+https://'): - raise - - return self._create_glare_httpsconnectionpool(url) - - def cert_verify(self, conn, url, verify, cert): - super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert) - conn.ca_certs = verify[0] - conn.insecure = verify[1] - - -class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool): - """A replacement for the default HTTPSConnectionPool. - - HTTPSConnectionPool will be instantiated when a new - connection is requested to the HTTPSAdapter. This - implementation overwrites the _new_conn method and - returns an instances of glareclient's VerifiedHTTPSConnection - which handles no compression. - - ssl_compression is hard-coded to False because this will - be used just when the user sets --no-ssl-compression. - """ - - scheme = 'glare+https' - - def _new_conn(self): - self.num_connections += 1 - return VerifiedHTTPSConnection(host=self.host, - port=self.port, - key_file=self.key_file, - cert_file=self.cert_file, - cacert=self.ca_certs, - insecure=self.insecure, - ssl_compression=False) - - -class OpenSSLConnectionDelegator(object): - """An OpenSSL.SSL.Connection delegator. - - Supplies an additional 'makefile' method which httplib requires - and is not present in OpenSSL.SSL.Connection. - - Note: Since it is not possible to inherit from OpenSSL.SSL.Connection - a delegator must be used. - """ - def __init__(self, *args, **kwargs): - self.connection = Connection(*args, **kwargs) - - def __getattr__(self, name): - return getattr(self.connection, name) - - def makefile(self, *args, **kwargs): - return socket._fileobject(self.connection, *args, **kwargs) - - -class VerifiedHTTPSConnection(HTTPSConnection): - """Extended OpenSSL HTTPSConnection for enhanced SSL support. - - Note: Much of this functionality can eventually be replaced - with native Python 3.3 code. - """ - # Restrict the set of client supported cipher suites - CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\ - 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\ - 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS' - - def __init__(self, host, port=None, key_file=None, cert_file=None, - cacert=None, timeout=None, insecure=False, - ssl_compression=True): - # List of exceptions reported by Python3 instead of - # SSLConfigurationError - if six.PY3: - excp_lst = (TypeError, FileNotFoundError, ssl.SSLError) - else: - # NOTE(jamespage) - # Accommodate changes in behaviour for pep-0467, introduced - # in python 2.7.9. - # https://github.com/python/peps/blob/master/pep-0476.txt - excp_lst = (TypeError, IOError, ssl.SSLError) - try: - HTTPSConnection.__init__(self, host, port, - key_file=key_file, - cert_file=cert_file) - self.key_file = key_file - self.cert_file = cert_file - self.timeout = timeout - self.insecure = insecure - # NOTE(flaper87): `is_verified` is needed for - # requests' urllib3. If insecure is True then - # the request is not `verified`, hence `not insecure` - self.is_verified = not insecure - self.ssl_compression = ssl_compression - self.cacert = None if cacert is None else str(cacert) - self.set_context() - # ssl exceptions are reported in various form in Python 3 - # so to be compatible, we report the same kind as under - # Python2 - except excp_lst as e: - raise exc.SSLConfigurationError(str(e)) - - def set_context(self): - """Set up the OpenSSL context.""" - self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - self.context.set_cipher_list(self.CIPHERS) - - if self.ssl_compression is False: - self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION - - if self.insecure is not True: - self.context.set_verify(OpenSSL.SSL.VERIFY_PEER, - verify_callback(host=self.host)) - else: - self.context.set_verify(OpenSSL.SSL.VERIFY_NONE, - lambda *args: True) - - if self.cert_file: - try: - self.context.use_certificate_file(self.cert_file) - except Exception as e: - msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e) - raise exc.SSLConfigurationError(msg) - if self.key_file is None: - # We support having key and cert in same file - try: - self.context.use_privatekey_file(self.cert_file) - except Exception as e: - msg = ('No key file specified and unable to load key ' - 'from "%s" %s' % (self.cert_file, e)) - raise exc.SSLConfigurationError(msg) - - if self.key_file: - try: - self.context.use_privatekey_file(self.key_file) - except Exception as e: - msg = 'Unable to load key from "%s" %s' % (self.key_file, e) - raise exc.SSLConfigurationError(msg) - - if self.cacert: - try: - self.context.load_verify_locations(to_bytes(self.cacert)) - except Exception as e: - msg = 'Unable to load CA from "%s" %s' % (self.cacert, e) - raise exc.SSLConfigurationError(msg) - else: - self.context.set_default_verify_paths() - - def connect(self): - """Connect to an SSL port using the OpenSSL library. - - This method also applies per-connection parameters to the connection. - """ - result = socket.getaddrinfo(self.host, self.port, 0, - socket.SOCK_STREAM) - if result: - socket_family = result[0][0] - if socket_family == socket.AF_INET6: - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - else: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - else: - # If due to some reason the address lookup fails - we still connect - # to IPv4 socket. This retains the older behavior. - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if self.timeout is not None: - # '0' microseconds - sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, - struct.pack('LL', self.timeout, 0)) - self.sock = OpenSSLConnectionDelegator(self.context, sock) - self.sock.connect((self.host, self.port)) diff --git a/glareclient/tests/unit/fakes.py b/glareclient/tests/unit/fakes.py new file mode 100644 index 0000000..d0986df --- /dev/null +++ b/glareclient/tests/unit/fakes.py @@ -0,0 +1,48 @@ +# 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. + +from oslo_serialization import jsonutils + + +class FakeRaw(object): + version = 110 + + +class FakeHTTPResponse(object): + + version = 1.1 + + def __init__(self, status_code, reason, headers, content): + self.headers = headers + self.content = content + self.text = content + self.status_code = status_code + self.reason = reason + self.raw = FakeRaw() + + def getheader(self, name, default=None): + return self.headers.get(name, default) + + def getheaders(self): + return self.headers.items() + + def read(self, amt=None): + b = self.content + self.content = None + return b + + def iter_content(self, chunksize): + return self.content + + def json(self): + return jsonutils.loads(self.content) diff --git a/glareclient/tests/unit/test_client.py b/glareclient/tests/unit/test_client.py deleted file mode 100644 index 2c80838..0000000 --- a/glareclient/tests/unit/test_client.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2014 Red Hat, Inc. -# All Rights Reserved. -# -# 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 testtools - -from glareclient import client -from glareclient import v1 - - -class ClientTest(testtools.TestCase): - - def test_no_endpoint_error(self): - self.assertRaises(ValueError, client.Client, None) - - def test_endpoint(self): - gc = client.Client('1', "http://example.com") - self.assertEqual("http://example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) - - def test_versioned_endpoint(self): - gc = client.Client('1', "http://example.com/v1") - self.assertEqual("http://example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) - - def test_versioned_endpoint_with_minor_revision(self): - gc = client.Client('1', "http://example.com/v1.0") - self.assertEqual("http://example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) - - def test_endpoint_with_version_hostname(self): - gc = client.Client('1', "http://v1.example.com") - self.assertEqual("http://v1.example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) - - def test_versioned_endpoint_with_version_hostname_v1(self): - gc = client.Client(endpoint="http://v2.example.com/v1") - self.assertEqual("http://v2.example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) - - def test_versioned_endpoint_with_minor_revision_and_version_hostname(self): - gc = client.Client(endpoint="http://v1.example.com/v1.1") - self.assertEqual("http://v1.example.com", gc.http_client.endpoint) - self.assertIsInstance(gc, v1.client.Client) diff --git a/glareclient/tests/unit/test_common_http.py b/glareclient/tests/unit/test_common_http.py new file mode 100644 index 0000000..f91edee --- /dev/null +++ b/glareclient/tests/unit/test_common_http.py @@ -0,0 +1,483 @@ +# -*- coding:utf-8 -*- +# 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 socket + +import mock +import testtools + +from glareclient.common import exceptions as exc +from glareclient.common import http +from glareclient.tests.unit import fakes + + +@mock.patch('glareclient.common.http.requests.request') +class HttpClientTest(testtools.TestCase): + + # Patch os.environ to avoid required auth info. + def setUp(self): + super(HttpClientTest, self).setUp() + + def test_http_raw_request(self, mock_request): + headers = {'User-Agent': 'python-glareclient'} + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {}, + '') + + client = http.HTTPClient('http://example.com:9494') + resp = client.request('', 'GET') + self.assertEqual(200, resp.status_code) + self.assertEqual('', ''.join([x for x in resp.content])) + mock_request.assert_called_with('GET', 'http://example.com:9494', + allow_redirects=False, + headers=headers) + + def test_token_or_credentials(self, mock_request): + # Record a 200 + fake200 = fakes.FakeHTTPResponse( + 200, 'OK', + {}, + '') + + mock_request.side_effect = [fake200, fake200, fake200] + + # Replay, create client, assert + client = http.HTTPClient('http://example.com:9494') + resp = client.request('', 'GET') + self.assertEqual(200, resp.status_code) + + client.username = 'user' + client.password = 'pass' + resp = client.request('', 'GET') + self.assertEqual(200, resp.status_code) + + client.auth_token = 'abcd1234' + resp = client.request('', 'GET') + self.assertEqual(200, resp.status_code) + + # no token or credentials + mock_request.assert_has_calls([ + mock.call('GET', 'http://example.com:9494', + allow_redirects=False, + headers={'User-Agent': 'python-glareclient'}), + mock.call('GET', 'http://example.com:9494', + allow_redirects=False, + headers={'User-Agent': 'python-glareclient', + 'X-Auth-Key': 'pass', + 'X-Auth-User': 'user'}), + mock.call('GET', 'http://example.com:9494', + allow_redirects=False, + headers={'User-Agent': 'python-glareclient', + 'X-Auth-Token': 'abcd1234'}) + ]) + + def test_region_name(self, mock_request): + # Record a 200 + fake200 = fakes.FakeHTTPResponse( + 200, 'OK', + {}, + '') + + mock_request.return_value = fake200 + + client = http.HTTPClient('http://example.com:9494') + client.region_name = 'RegionOne' + resp = client.request('', 'GET') + self.assertEqual(200, resp.status_code) + + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + headers={'X-Region-Name': 'RegionOne', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request(self, mock_request): + # Record a 200 + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}') + client = http.HTTPClient('http://example.com:9494') + resp, body = client.json_request('', 'GET') + self.assertEqual(200, resp.status_code) + self.assertEqual({}, body) + + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request_argument_passed_to_requests(self, mock_request): + """Check that we have sent the proper arguments to requests.""" + # Record a 200 + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}') + + client = http.HTTPClient('http://example.com:9494') + client.verify_cert = True + client.cert_file = 'RANDOM_CERT_FILE' + client.key_file = 'RANDOM_KEY_FILE' + client.auth_url = 'http://AUTH_URL' + resp, body = client.json_request('', 'GET', data='text') + self.assertEqual(200, resp.status_code) + self.assertEqual({}, body) + + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'), + verify=True, + data='text', + stream=False, + headers={'Content-Type': 'application/json', + 'X-Auth-Url': 'http://AUTH_URL', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request_w_req_body(self, mock_request): + # Record a 200 + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}') + + client = http.HTTPClient('http://example.com:9494') + resp, body = client.json_request('', 'GET', data='test-body') + self.assertEqual(200, resp.status_code) + self.assertEqual({}, body) + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + data='test-body', + allow_redirects=False, + stream=False, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request_non_json_resp_cont_type(self, mock_request): + # Record a 200 + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'not/json'}, + '{}') + + client = http.HTTPClient('http://example.com:9494') + resp, body = client.json_request('', 'GET', data='test-data') + self.assertEqual(200, resp.status_code) + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', data='test-data', + allow_redirects=False, + stream=False, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request_invalid_json(self, mock_request): + # Record a 200 + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + 'invalid-json') + + client = http.HTTPClient('http://example.com:9494') + resp, body = client.json_request('', 'GET') + self.assertEqual(200, resp.status_code) + self.assertIsNone(body) + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_manual_redirect_delete(self, mock_request): + mock_request.side_effect = [ + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:9494/foo/bar'}, + ''), + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')] + + client = http.HTTPClient('http://example.com:9494/foo') + resp, body = client.json_request('', 'DELETE') + + self.assertEqual(200, resp.status_code) + mock_request.assert_has_calls([ + mock.call('DELETE', 'http://example.com:9494/foo', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}), + mock.call('DELETE', 'http://example.com:9494/foo/bar', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + ]) + + def test_http_manual_redirect_post(self, mock_request): + mock_request.side_effect = [ + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:9494/foo/bar'}, + ''), + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')] + + client = http.HTTPClient('http://example.com:9494/foo') + resp, body = client.json_request('', 'POST') + + self.assertEqual(200, resp.status_code) + mock_request.assert_has_calls([ + mock.call('POST', 'http://example.com:9494/foo', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}), + mock.call('POST', 'http://example.com:9494/foo/bar', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + ]) + + def test_http_manual_redirect_put(self, mock_request): + mock_request.side_effect = [ + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:9494/foo/bar'}, + ''), + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')] + + client = http.HTTPClient('http://example.com:9494/foo') + resp, body = client.json_request('', 'PUT') + + self.assertEqual(200, resp.status_code) + mock_request.assert_has_calls([ + mock.call('PUT', 'http://example.com:9494/foo', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}), + mock.call('PUT', 'http://example.com:9494/foo/bar', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + ]) + + def test_http_manual_redirect_prohibited(self, mock_request): + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:9494/'}, + '') + client = http.HTTPClient('http://example.com:9494/foo') + self.assertRaises(exc.InvalidEndpoint, + client.json_request, '', 'DELETE') + mock_request.assert_called_once_with( + 'DELETE', 'http://example.com:9494/foo', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_manual_redirect_error_without_location(self, mock_request): + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 302, 'Found', + {}, + '') + client = http.HTTPClient('http://example.com:9494/foo') + self.assertRaises(exc.InvalidEndpoint, + client.json_request, '', 'DELETE') + mock_request.assert_called_once_with( + 'DELETE', 'http://example.com:9494/foo', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_json_request_redirect(self, mock_request): + # Record the 302 + mock_request.side_effect = [ + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:9494'}, + ''), + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')] + + client = http.HTTPClient('http://example.com:9494') + resp, body = client.json_request('', 'GET') + self.assertEqual(200, resp.status_code) + self.assertEqual({}, body) + + mock_request.assert_has_calls([ + mock.call('GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}), + mock.call('GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + ]) + + def test_http_404_json_request(self, mock_request): + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 404, 'Not Found', {'content-type': 'application/json'}, + '{}') + + client = http.HTTPClient('http://example.com:9494') + e = self.assertRaises(exc.HTTPNotFound, client.json_request, '', 'GET') + # Assert that the raised exception can be converted to string + self.assertIsNotNone(str(e)) + # Record a 404 + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_http_300_json_request(self, mock_request): + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 300, 'OK', {'content-type': 'application/json'}, + '{}') + client = http.HTTPClient('http://example.com:9494') + e = self.assertRaises( + exc.HTTPMultipleChoices, client.json_request, '', 'GET') + # Assert that the raised exception can be converted to string + self.assertIsNotNone(str(e)) + + # Record a 300 + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}) + + def test_fake_json_request(self, mock_request): + headers = {'User-Agent': 'python-glareclient'} + mock_request.side_effect = [socket.gaierror] + + client = http.HTTPClient('fake://example.com:9494') + self.assertRaises(exc.InvalidEndpoint, + client.request, "/", "GET") + mock_request.assert_called_once_with('GET', 'fake://example.com:9494/', + allow_redirects=False, + headers=headers) + + def test_http_request_socket_error(self, mock_request): + headers = {'User-Agent': 'python-glareclient'} + mock_request.side_effect = [socket.gaierror] + + client = http.HTTPClient('http://example.com:9494') + self.assertRaises(exc.InvalidEndpoint, + client.request, "/", "GET") + mock_request.assert_called_once_with('GET', 'http://example.com:9494/', + allow_redirects=False, + headers=headers) + + def test_http_request_socket_timeout(self, mock_request): + headers = {'User-Agent': 'python-glareclient'} + mock_request.side_effect = [socket.timeout] + + client = http.HTTPClient('http://example.com:9494') + self.assertRaises(exc.CommunicationError, + client.request, "/", "GET") + mock_request.assert_called_once_with('GET', 'http://example.com:9494/', + allow_redirects=False, + headers=headers) + + def test_http_request_specify_timeout(self, mock_request): + mock_request.return_value = \ + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}') + + client = http.HTTPClient('http://example.com:9494', timeout='123') + resp, body = client.json_request('', 'GET') + self.assertEqual(200, resp.status_code) + self.assertEqual({}, body) + mock_request.assert_called_once_with( + 'GET', 'http://example.com:9494', + allow_redirects=False, + stream=False, + data=None, + headers={'Content-Type': 'application/json', + 'User-Agent': 'python-glareclient'}, + timeout=float(123)) + + def test_get_system_ca_file(self, mock_request): + chosen = '/etc/ssl/certs/ca-certificates.crt' + with mock.patch('os.path.exists') as mock_os: + mock_os.return_value = chosen + + ca = http.get_system_ca_file() + self.assertEqual(chosen, ca) + + mock_os.assert_called_once_with(chosen) + + def test_insecure_verify_cert_None(self, mock_request): + client = http.HTTPClient('https://foo', insecure=True) + self.assertFalse(client.verify_cert) + + def test_passed_cert_to_verify_cert(self, mock_request): + client = http.HTTPClient('https://foo', cacert="NOWHERE") + self.assertEqual("NOWHERE", client.verify_cert) + + with mock.patch('glareclient.common.http.get_system_ca_file') as gsf: + gsf.return_value = "SOMEWHERE" + client = http.HTTPClient('https://foo') + self.assertEqual("SOMEWHERE", client.verify_cert) diff --git a/glareclient/tests/unit/test_http.py b/glareclient/tests/unit/test_http.py deleted file mode 100644 index bd0cacd..0000000 --- a/glareclient/tests/unit/test_http.py +++ /dev/null @@ -1,401 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 functools -import json - -from keystoneauth1 import session -from keystoneauth1 import token_endpoint - -import mock -import requests -from requests_mock.contrib import fixture -import six -from six.moves.urllib import parse -from testscenarios import load_tests_apply_scenarios as load_tests # noqa -import testtools -from testtools import matchers -import types - -import glareclient -from glareclient.common import http -from glareclient.tests import utils - - -def original_only(f): - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - if not hasattr(self.client, 'log_curl_request'): - self.skipTest('Skip logging tests for session client') - - return f(self, *args, **kwargs) - - -class TestClient(testtools.TestCase): - - scenarios = [ - ('httpclient', {'create_client': '_create_http_client'}), - ('session', {'create_client': '_create_session_client'}) - ] - - def _create_http_client(self): - return http.HTTPClient(self.endpoint, token=self.token) - - def _create_session_client(self): - auth = token_endpoint.Token(self.endpoint, self.token) - sess = session.Session(auth=auth) - return http.SessionClient(sess) - - def setUp(self): - super(TestClient, self).setUp() - self.mock = self.useFixture(fixture.Fixture()) - - self.endpoint = 'http://example.com:9292' - self.ssl_endpoint = 'https://example.com:9292' - self.token = u'abc123' - - self.client = getattr(self, self.create_client)() - - def test_identity_headers_and_token(self): - identity_headers = { - 'X-Auth-Token': 'auth_token', - 'X-User-Id': 'user', - 'X-Tenant-Id': 'tenant', - 'X-Roles': 'roles', - 'X-Identity-Status': 'Confirmed', - 'X-Service-Catalog': 'service_catalog', - } - # with token - kwargs = {'token': u'fake-token', - 'identity_headers': identity_headers} - http_client_object = http.HTTPClient(self.endpoint, **kwargs) - self.assertEqual('auth_token', http_client_object.auth_token) - self.assertTrue(http_client_object.identity_headers. - get('X-Auth-Token') is None) - - def test_identity_headers_and_no_token_in_header(self): - identity_headers = { - 'X-User-Id': 'user', - 'X-Tenant-Id': 'tenant', - 'X-Roles': 'roles', - 'X-Identity-Status': 'Confirmed', - 'X-Service-Catalog': 'service_catalog', - } - # without X-Auth-Token in identity headers - kwargs = {'token': u'fake-token', - 'identity_headers': identity_headers} - http_client_object = http.HTTPClient(self.endpoint, **kwargs) - self.assertEqual(u'fake-token', http_client_object.auth_token) - self.assertTrue(http_client_object.identity_headers. - get('X-Auth-Token') is None) - - def test_identity_headers_and_no_token_in_session_header(self): - # Tests that if token or X-Auth-Token are not provided in the kwargs - # when creating the http client, the session headers don't contain - # the X-Auth-Token key. - identity_headers = { - 'X-User-Id': 'user', - 'X-Tenant-Id': 'tenant', - 'X-Roles': 'roles', - 'X-Identity-Status': 'Confirmed', - 'X-Service-Catalog': 'service_catalog', - } - kwargs = {'identity_headers': identity_headers} - http_client_object = http.HTTPClient(self.endpoint, **kwargs) - self.assertIsNone(http_client_object.auth_token) - self.assertNotIn('X-Auth-Token', http_client_object.session.headers) - - def test_identity_headers_are_passed(self): - # Tests that if token or X-Auth-Token are not provided in the kwargs - # when creating the http client, the session headers don't contain - # the X-Auth-Token key. - identity_headers = { - 'X-User-Id': b'user', - 'X-Tenant-Id': b'tenant', - 'X-Roles': b'roles', - 'X-Identity-Status': b'Confirmed', - 'X-Service-Catalog': b'service_catalog', - } - kwargs = {'identity_headers': identity_headers} - http_client = http.HTTPClient(self.endpoint, **kwargs) - - path = '/artifactsmy-image' - self.mock.get(self.endpoint + path) - http_client.get(path) - - headers = self.mock.last_request.headers - for k, v in six.iteritems(identity_headers): - self.assertEqual(v, headers[k]) - - def test_language_header_passed(self): - kwargs = {'language_header': 'nb_NO'} - http_client = http.HTTPClient(self.endpoint, **kwargs) - - path = '/v2/images/my-image' - self.mock.get(self.endpoint + path) - http_client.get(path) - - headers = self.mock.last_request.headers - self.assertEqual(kwargs['language_header'], headers['Accept-Language']) - - def test_language_header_not_passed_no_language(self): - kwargs = {} - http_client = http.HTTPClient(self.endpoint, **kwargs) - - path = '/v2/images/my-image' - self.mock.get(self.endpoint + path) - http_client.get(path) - - headers = self.mock.last_request.headers - self.assertTrue('Accept-Language' not in headers) - - def test_connection_timeout(self): - """Verify a InvalidEndpoint is received if connection times out.""" - def cb(request, context): - raise requests.exceptions.Timeout - - path = '/v1/images' - self.mock.get(self.endpoint + path, text=cb) - comm_err = self.assertRaises(glareclient.exc.InvalidEndpoint, - self.client.get, - '/v1/images') - self.assertIn(self.endpoint, comm_err.message) - - def test_connection_refused(self): - """Verify a CommunicationError is received if connection is refused. - - The error should list the host and port that refused the connection. - """ - def cb(request, context): - raise requests.exceptions.ConnectionError() - - path = '/artifacts/?limit=20' - self.mock.get(self.endpoint + path, text=cb) - - comm_err = self.assertRaises(glareclient.exc.CommunicationError, - self.client.get, - '/artifacts/?limit=20') - - self.assertIn(self.endpoint, comm_err.message) - - def test_http_encoding(self): - path = '/artifacts/' - text = 'Ok' - self.mock.get(self.endpoint + path, text=text, - headers={"Content-Type": "text/plain"}) - - headers = {"test": u'ni\xf1o'} - resp, body = self.client.get(path, headers=headers) - self.assertEqual(text, resp.text) - - def test_request_id(self): - path = '/artifacts/' - self.mock.get(self.endpoint + path, - headers={"x-openstack-request-id": "req-aaa"}) - - self.client.get(path) - self.assertEqual(self.client.last_request_id, 'req-aaa') - - def test_headers_encoding(self): - value = u'ni\xf1o' - headers = {"test": value, "none-val": None} - encoded = http.encode_headers(headers) - self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"]) - self.assertNotIn("none-val", encoded) - - def test_raw_request(self): - """Verify the path being used for HTTP requests reflects accurately.""" - headers = {"Content-Type": "text/plain"} - text = 'Ok' - path = '/artifacts/' - - self.mock.get(self.endpoint + path, text=text, headers=headers) - - resp, body = self.client.get('/artifacts/', headers=headers) - self.assertEqual(headers, resp.headers) - self.assertEqual(text, resp.text) - - def test_parse_endpoint(self): - endpoint = 'http://example.com:9292' - test_client = http.HTTPClient(endpoint, token=u'adc123') - actual = test_client.parse_endpoint(endpoint) - expected = parse.SplitResult(scheme='http', - netloc='example.com:9292', path='', - query='', fragment='') - self.assertEqual(expected, actual) - - def test_get_connections_kwargs_http(self): - endpoint = 'http://example.com:9292' - test_client = http.HTTPClient(endpoint, token=u'adc123') - self.assertEqual(600.0, test_client.timeout) - - def test__chunk_body_exact_size_chunk(self): - test_client = http._BaseHTTPClient() - bytestring = b'x' * http.CHUNKSIZE - data = six.BytesIO(bytestring) - chunk = list(test_client._chunk_body(data)) - self.assertEqual(1, len(chunk)) - self.assertEqual([bytestring], chunk) - - def test_http_chunked_request(self): - text = "Ok" - data = six.StringIO(text) - path = '/artifacts' - self.mock.post(self.endpoint + path, text=text) - - headers = {"test": u'chunked_request'} - resp, body = self.client.post(path, headers=headers, data=data) - self.assertIsInstance(self.mock.last_request.body, types.GeneratorType) - self.assertEqual(text, resp.text) - - def test_http_json(self): - data = {"test": "json_request"} - path = '/artifacts' - text = 'OK' - self.mock.post(self.endpoint + path, text=text) - - headers = {"test": u'chunked_request'} - resp, body = self.client.post(path, headers=headers, data=data) - - self.assertEqual(text, resp.text) - self.assertIsInstance(self.mock.last_request.body, six.string_types) - self.assertEqual(data, json.loads(self.mock.last_request.body)) - - @original_only - def test_log_http_response_with_non_ascii_char(self): - try: - response = 'Ok' - headers = {"Content-Type": "text/plain", - "test": "value1\xa5\xa6"} - fake = utils.FakeResponse(headers, six.StringIO(response)) - self.client.log_http_response(fake) - except UnicodeDecodeError as e: - self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) - - @original_only - def test_log_curl_request_with_non_ascii_char(self): - try: - headers = {'header1': 'value1\xa5\xa6'} - body = 'examplebody\xa5\xa6' - self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body, - None) - except UnicodeDecodeError as e: - self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) - - @original_only - @mock.patch('glareclient.common.http.LOG.debug') - def test_log_curl_request_with_body_and_header(self, mock_log): - hd_name = 'header1' - hd_val = 'value1' - headers = {hd_name: hd_val} - body = 'examplebody' - self.client.log_curl_request('GET', '/api/v1/', headers, body, None) - self.assertTrue(mock_log.called, 'LOG.debug never called') - self.assertTrue(mock_log.call_args[0], - 'LOG.debug called with no arguments') - hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val) - self.assertThat(mock_log.call_args[0][0], - matchers.MatchesRegex(hd_regex), - 'header not found in curl command') - body_regex = ".*\s-d\s+'%s'\s.*" % body - self.assertThat(mock_log.call_args[0][0], - matchers.MatchesRegex(body_regex), - 'body not found in curl command') - - def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert): - headers = {'header1': 'value1'} - http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key, - cert_file=cert, cacert=cacert, - token='fake-token') - http_client_object.log_curl_request('GET', '/api/v1/', headers, None, - None) - self.assertTrue(mock_log.called, 'LOG.debug never called') - self.assertTrue(mock_log.call_args[0], - 'LOG.debug called with no arguments') - - needles = {'key': key, 'cert': cert, 'cacert': cacert} - for option, value in six.iteritems(needles): - if value: - regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value) - self.assertThat(mock_log.call_args[0][0], - matchers.MatchesRegex(regex), - 'no --%s option in curl command' % option) - else: - regex = ".*\s--%s\s+.*" % option - self.assertThat(mock_log.call_args[0][0], - matchers.Not(matchers.MatchesRegex(regex)), - 'unexpected --%s option in curl command' % - option) - - @mock.patch('glareclient.common.http.LOG.debug') - def test_log_curl_request_with_all_certs(self, mock_log): - self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', - 'cacert2') - - @mock.patch('glareclient.common.http.LOG.debug') - def test_log_curl_request_with_some_certs(self, mock_log): - self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None) - - @mock.patch('glareclient.common.http.LOG.debug') - def test_log_curl_request_with_insecure_param(self, mock_log): - headers = {'header1': 'value1'} - http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True, - token='fake-token') - http_client_object.log_curl_request('GET', '/api/v1/', headers, None, - None) - self.assertTrue(mock_log.called, 'LOG.debug never called') - self.assertTrue(mock_log.call_args[0], - 'LOG.debug called with no arguments') - self.assertThat(mock_log.call_args[0][0], - matchers.MatchesRegex('.*\s-k\s.*'), - 'no -k option in curl command') - - @mock.patch('glareclient.common.http.LOG.debug') - def test_log_curl_request_with_token_header(self, mock_log): - fake_token = 'fake-token' - headers = {'X-Auth-Token': fake_token} - http_client_object = http.HTTPClient(self.endpoint, - identity_headers=headers) - http_client_object.log_curl_request('GET', '/api/v1/', headers, None, - None) - self.assertTrue(mock_log.called, 'LOG.debug never called') - self.assertTrue(mock_log.call_args[0], - 'LOG.debug called with no arguments') - token_regex = '.*%s.*' % fake_token - self.assertThat(mock_log.call_args[0][0], - matchers.Not(matchers.MatchesRegex(token_regex)), - 'token found in LOG.debug parameter') - - def test_expired_token_has_changed(self): - # instantiate client with some token - fake_token = b'fake-token' - http_client = http.HTTPClient(self.endpoint, - token=fake_token) - path = '/artifacts' - self.mock.get(self.endpoint + path) - http_client.get(path) - headers = self.mock.last_request.headers - self.assertEqual(fake_token, headers['X-Auth-Token']) - # refresh the token - refreshed_token = b'refreshed-token' - http_client.auth_token = refreshed_token - http_client.get(path) - headers = self.mock.last_request.headers - self.assertEqual(refreshed_token, headers['X-Auth-Token']) - # regression check for bug 1448080 - unicode_token = u'ni\xf1o' - http_client.auth_token = unicode_token - http_client.get(path) - headers = self.mock.last_request.headers - self.assertEqual(b'ni\xc3\xb1o', headers['X-Auth-Token']) diff --git a/glareclient/tests/unit/test_ssl.py b/glareclient/tests/unit/test_ssl.py deleted file mode 100644 index 94a7557..0000000 --- a/glareclient/tests/unit/test_ssl.py +++ /dev/null @@ -1,226 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# 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 os - -import mock -import six -import ssl -import testtools -import threading - -from glareclient import Client -from glareclient import exc -from glareclient import v1 - -if six.PY3 is True: - import socketserver -else: - import SocketServer as socketserver - - -TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'var')) - - -class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): - def handle(self): - self.request.recv(1024) - response = b'somebytes' - self.request.sendall(response) - - -class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): - def get_request(self): - key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key') - cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') - cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') - (_sock, addr) = socketserver.TCPServer.get_request(self) - sock = ssl.wrap_socket(_sock, - certfile=cert_file, - keyfile=key_file, - ca_certs=cacert, - server_side=True, - cert_reqs=ssl.CERT_REQUIRED) - return sock, addr - - -class TestHTTPSVerifyCert(testtools.TestCase): - """Check 'requests' based ssl verification occurs. - - The requests library performs SSL certificate validation, - however there is still a need to check that the glare - client is properly integrated with requests so that - cert validation actually happens. - """ - def setUp(self): - # Rather than spinning up a new process, we create - # a thread to perform client/server interaction. - # This should run more quickly. - super(TestHTTPSVerifyCert, self).setUp() - server = ThreadedTCPServer(('127.0.0.1', 0), - ThreadedTCPRequestHandler) - __, self.port = server.server_address - server_thread = threading.Thread(target=server.serve_forever) - server_thread.daemon = True - server_thread.start() - - @mock.patch('sys.stderr') - def test_v1_requests_cert_verification(self, __): - """v1 regression test for bug 115260.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - - try: - client = v1.Client(url, - insecure=False, - ssl_compression=True) - client.artifacts.get('123', type_name='sample_artifact') - self.fail('No SSL exception has been raised') - except exc.CommunicationError as e: - if 'certificate verify failed' not in e.message: - self.fail('No certificate failure message is received') - except Exception: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_cert_verification_no_compression(self, __): - """v1 regression test for bug 115260.""" - # Legacy test. Verify 'no compression' has no effect - port = self.port - url = 'https://0.0.0.0:%d' % port - - try: - client = v1.Client(url, - insecure=False, - ssl_compression=False) - client.artifacts.get('123', type_name='sample_artifact') - self.fail('No SSL exception has been raised') - except exc.CommunicationError as e: - if 'certificate verify failed' not in e.message: - self.fail('No certificate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_valid_cert_verification(self, __): - """Test absence of SSL key file.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') - - try: - gc = Client('1', url, - insecure=False, - ssl_compression=True, - cacert=cacert) - gc.artifacts.get('123', type_name='sample_artifact') - except exc.CommunicationError as e: - if 'certificate verify failed' in e.message: - self.fail('Certificate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_valid_cert_verification_no_compression(self, __): - """Test VerifiedHTTPSConnection: absence of SSL key file.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') - - try: - gc = Client('1', url, - insecure=False, - ssl_compression=False, - cacert=cacert) - gc.artifacts.get('123', type_name='sample_artifact') - except exc.CommunicationError as e: - if 'certificate verify failed' in e.message: - self.fail('Certificate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_valid_cert_no_key(self, __): - """Test VerifiedHTTPSConnection: absence of SSL key file.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') - cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') - - try: - gc = Client('1', url, - insecure=False, - ssl_compression=False, - cert_file=cert_file, - cacert=cacert) - gc.artifacts.get('123', type_name='sample_artifact') - except exc.CommunicationError as e: - if ('PEM lib' not in e.message): - self.fail('No appropriate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_bad_cert(self, __): - """Test VerifiedHTTPSConnection: absence of SSL key file.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt') - cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') - - try: - gc = Client('1', url, - insecure=False, - ssl_compression=False, - cert_file=cert_file, - cacert=cacert) - gc.artifacts.get('123', type_name='sample_artifact') - except exc.CommunicationError as e: - # NOTE(dsariel) - # starting from python 2.7.8 the way to handle loading private - # keys into the SSL_CTX was changed and error message become - # similar to the one in 3.X - if (six.PY2 and 'PrivateKey' not in e.message and - 'PEM lib' not in e.message or - six.PY3 and 'PEM lib' not in e.message): - self.fail('No appropriate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') - - @mock.patch('sys.stderr') - def test_v1_requests_bad_ca(self, __): - """Test VerifiedHTTPSConnection: absence of SSL key file.""" - port = self.port - url = 'https://0.0.0.0:%d' % port - cacert = os.path.join(TEST_VAR_DIR, 'badca.crt') - - try: - gc = Client('1', url, - insecure=False, - ssl_compression=False, - cacert=cacert) - gc.artifacts.get('123', type_name='sample_artifact') - except exc.CommunicationError as e: - # NOTE(dsariel) - # starting from python 2.7.8 the way of handling x509 certificates - # was changed (github.com/python/peps/blob/master/pep-0476.txt#L28) - # and error message become similar to the one in 3.X - if (six.PY2 and 'certificate' not in e.message and - 'No such file' not in e.message or - six.PY3 and 'No such file' not in e.message): - self.fail('No appropriate failure message is received') - except Exception as e: - self.fail('Unexpected exception has been raised') diff --git a/glareclient/v1/client.py b/glareclient/v1/client.py index 158ee76..da77caf 100644 --- a/glareclient/v1/client.py +++ b/glareclient/v1/client.py @@ -1,3 +1,4 @@ + # Copyright 2012 OpenStack Foundation # All Rights Reserved. # @@ -13,28 +14,22 @@ # License for the specific language governing permissions and limitations # under the License. - from glareclient.common import http -from glareclient.common import utils from glareclient.v1 import artifacts from glareclient.v1 import versions class Client(object): - """Client for the Glare Artifact Repository v2 API. + """Client for the Glare Artifact Repository v1 API. - :param string endpoint: A user-supplied endpoint URL for the glare - service. + :param string endpoint: A user-supplied endpoint URL for the glare service. :param string token: Token for authentication. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) - :param string language_header: Set Accept-Language header to be sent in - requests to glare. """ - def __init__(self, endpoint=None, **kwargs): - endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0) - self.http_client = http.get_http_client(endpoint=endpoint, **kwargs) + def __init__(self, endpoint, **kwargs): + """Initialize a new client for the Glare v1 API.""" + self.version = kwargs.get('version') + self.http_client = http.construct_http_client(endpoint, **kwargs) self.artifacts = artifacts.Controller(self.http_client) self.versions = versions.VersionController(self.http_client) diff --git a/requirements.txt b/requirements.txt index 8f7a696..e6b9fc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ requests>=2.10.0 # Apache-2.0 six>=1.9.0 # MIT oslo.utils>=3.16.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 +oslo.log>=3.11.0 # Apache-2.0 osc-lib>=1.0.2 # Apache-2.0