Add keycloak auth support

Change-Id: I88c79656fbc6cd9c055569979083ef385ba84563
This commit is contained in:
Mike Fedosin 2017-02-03 17:27:19 +03:00
parent e7e555e045
commit 36f0e7318d
8 changed files with 496 additions and 55 deletions

View File

@ -27,6 +27,7 @@ import six
from six.moves import urllib
from glareclient.common import exceptions as exc
from glareclient.common import keycloak_auth
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-glareclient'
@ -59,27 +60,42 @@ def _handle_response(resp):
body_iter = jsonutils.loads(''.join([c for c in body_iter]))
except ValueError:
body_iter = None
elif content_type.startswith('application/json'):
elif 'json' in content_type:
# Let's use requests json method, it should take care of
# response encoding
body_iter = resp.json()
try:
body_iter = resp.json()
except Exception:
body_iter = None
else:
# Do not read all response in memory when downloading a blob.
body_iter = _close_after_stream(resp, CHUNKSIZE)
return resp, body_iter
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
for chunk in response.iter_content(chunk_size=chunk_size):
yield chunk
# Once we're done streaming the body, ensure everything is closed.
# This will return the connection to the HTTPConnectionPool in urllib3
# and ideally reduce the number of HTTPConnectionPool full warnings.
response.close()
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.auth_url = kwargs.get('auth_url')
self.auth_token = kwargs.get('token')
self.auth_token = kwargs.get('auth_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
self.tenant_name = kwargs.get('tenant_name')
self.cert_file = kwargs.get('cert_file')
self.key_file = kwargs.get('key_file')
@ -170,6 +186,8 @@ class HTTPClient(object):
kwargs['headers'].setdefault('X-Auth-Url', self.auth_url)
if self.region_name:
kwargs['headers'].setdefault('X-Region-Name', self.region_name)
if self.tenant_name:
kwargs['headers'].setdefault('X-Project-Id', self.tenant_name)
self.log_curl_request(url, method, kwargs)
@ -183,7 +201,7 @@ class HTTPClient(object):
kwargs['timeout'] = float(self.timeout)
# Allow the option not to follow redirects
follow_redirects = kwargs.pop('follow_redirects', True)
follow_redirects = kwargs.pop('redirect', 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
@ -194,7 +212,6 @@ class HTTPClient(object):
# point version i.e.: 3.x
# See issue: https://github.com/kennethreitz/requests/issues/1704
allow_redirects = False
try:
resp = requests.request(
method,
@ -255,31 +272,27 @@ class HTTPClient(object):
creds['X-Auth-Key'] = self.password
return creds
def json_request(self, url, method, **kwargs):
def process_request(self, url, method, **kwargs):
resp = self.request(url, method, **kwargs)
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)
return self.process_request(url, "HEAD", **kwargs)
def get(self, url, **kwargs):
return self.json_request(url, "GET", **kwargs)
return self.process_request(url, "GET", **kwargs)
def post(self, url, **kwargs):
return self.json_request(url, "POST", **kwargs)
return self.process_request(url, "POST", **kwargs)
def put(self, url, **kwargs):
return self.json_request(url, "PUT", **kwargs)
return self.process_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)
return self.process_request(url, "PATCH", **kwargs)
class SessionClient(adapter.LegacyJsonAdapter):
@ -299,7 +312,8 @@ def construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
endpoint = next(iter(args), None)
keycloak_auth_url = kwargs.pop('keycloak_auth_url', None)
auth_token = kwargs.pop('auth_token', None)
if session:
service_type = kwargs.pop('service_type', None)
endpoint_type = kwargs.pop('endpoint_type', None)
@ -318,6 +332,17 @@ def construct_http_client(*args, **kwargs):
parameters.update(kwargs)
return SessionClient(**parameters)
elif endpoint:
realm_name = kwargs.pop('keycloak_realm_name', None)
if keycloak_auth_url is not None:
kwargs['auth_token'] = keycloak_auth.authenticate(
auth_url=keycloak_auth_url,
client_id=kwargs.pop('openid_client_id', None),
username=kwargs.pop('keycloak_username', None),
password=kwargs.pop('keycloak_password', None),
realm_name=realm_name
)
else:
kwargs['auth_token'] = auth_token
return HTTPClient(endpoint, **kwargs)
else:
raise AttributeError('Constructing a client must contain either an '

View File

@ -0,0 +1,86 @@
# Copyright 2016 - Nokia Networks
#
# 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 pprint
import requests
LOG = logging.getLogger(__name__)
def authenticate(**kwargs):
"""Performs authentication using Keycloak OpenID Protocol.
:param auth_url: Base authentication url of KeyCloak server (e.g.
"https://my.keycloak:8443/auth"
:param client_id: Client ID (according to OpenID Connect protocol).
:param realm_name: KeyCloak realm name.
:param username: User name (Optional, if None then access_token must be
provided).
:param password: Password (Optional).
:param insecure: If True, SSL certificate is not verified (Optional).
"""
auth_url = kwargs.get('auth_url')
client_id = kwargs.get('client_id')
realm_name = kwargs.get('realm_name')
username = kwargs.get('username')
password = kwargs.get('password')
insecure = kwargs.get('insecure', False)
if not auth_url:
raise ValueError('Base authentication url is not provided.')
if not client_id:
raise ValueError('Client ID is not provided.')
if not realm_name:
raise ValueError('Project(realm) name is not provided.')
if not username:
raise ValueError('Username is not provided.')
if password is None:
raise ValueError('Password is not provided.')
access_token_endpoint = (
"%s/realms/%s/protocol/openid-connect/token" %
(auth_url, realm_name)
)
body = {
'grant_type': 'password',
'username': username,
'password': password,
'scope': 'profile',
'client_id': client_id
}
resp = requests.post(
access_token_endpoint,
data=body,
verify=not insecure
)
try:
resp.raise_for_status()
except Exception as e:
raise Exception("Failed to get access token:\n %s" % str(e))
LOG.debug(
"HTTP response from OIDC provider: %s" %
pprint.pformat(resp.json())
)
return resp.json()['access_token']

View File

@ -63,18 +63,19 @@ class ResponseBlobWrapper(object):
def __init__(self, resp, verify_md5=True):
self.hash_md5 = resp.headers.get("Content-MD5")
self.check_md5 = hashlib.md5()
self.blob_md5 = hashlib.md5()
if 301 <= resp.status_code <= 302:
# NOTE(sskripnick): handle redirect manually to prevent sending
# auth token to external resource.
# Use stream=True to prevent reading whole response into memory.
# Set Accept-Encoding explicitly to "identity" because setting
# stream=True forces Accept-Encoding to be "gzip, defalate".
# stream=True forces Accept-Encoding to be "gzip, deflate".
# It should be "identity" because we should know Content-Length.
resp = requests.get(resp.headers.get("Location"),
headers={"Accept-Encoding": "identity"})
self.len = resp.headers.get("Content-Length", 0)
self.iter = resp.iter_content(65536)
self.verify_md5 = verify_md5
def __iter__(self):
return self
@ -82,13 +83,14 @@ class ResponseBlobWrapper(object):
def next(self):
try:
data = self.iter.next()
self.check_md5.update(data)
if self.verify_md5:
self.blob_md5.update(data)
return data
except StopIteration:
if self.check_md5.hexdigest() != self.hash_md5:
if self.verify_md5 and self.blob_md5.hexdigest() != self.hash_md5:
raise IOError(errno.EPIPE,
'Checksum mismatch: %s (expected %s)' %
(self.check_md5.hexdigest(), self.hash_md5))
(self.blob_md5.hexdigest(), self.hash_md5))
raise
__next__ = next

View File

@ -34,14 +34,13 @@ def make_client(instance):
API_VERSIONS)
LOG.debug("Instantiating glare client: {0}".format(
glare_client))
client = glare_client(
instance.get_configuration().get('glare_url'),
kwargs = dict(
region_name=instance._region_name,
session=instance.session,
service_type='artifact',
)
return client
return glare_client(instance.get_configuration().get('glare_url'),
**kwargs)
def build_option_parser(parser):
@ -51,10 +50,10 @@ def build_option_parser(parser):
metavar='<artifact-api-version>',
default=utils.env('OS_ARTIFACT_API_VERSION'),
help=_('Artifact API version, default=%s '
'(Env: OS_ARTIFACT_API_VERSION)') % DEFAULT_API_VERSION,
)
parser.add_argument('--glare-url',
metavar='<GLARE_URL>',
default=utils.env('GLARE_URL'),
help='Defaults to env[GLARE_URL].')
'(Env: OS_ARTIFACT_API_VERSION)') % DEFAULT_API_VERSION)
parser.add_argument(
'--glare-url',
metavar='<GLARE_URL>',
default=utils.env('GLARE_URL'),
help='Defaults to env[GLARE_URL].')
return parser

320
glareclient/shell.py Normal file
View File

@ -0,0 +1,320 @@
# Copyright 2015 - StackStorm, Inc.
#
# 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.
"""
Command-line interface to the Glare APIs
"""
import argparse
import logging
import os
import sys
from cliff import app
from cliff import commandmanager
from osc_lib.command import command
from glareclient import client
from glareclient.common import utils
import glareclient.osc.v1.artifacts
import glareclient.osc.v1.blobs
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
class OpenStackHelpFormatter(argparse.HelpFormatter):
def __init__(self, prog, indent_increment=2, max_help_position=32,
width=None):
super(OpenStackHelpFormatter, self).__init__(
prog,
indent_increment,
max_help_position,
width
)
def start_section(self, heading):
# Title-case the headings.
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(OpenStackHelpFormatter, self).start_section(heading)
class HelpAction(argparse.Action):
"""Custom help action.
Provide a custom action so the -h and --help options
to the main app will print a list of the commands.
The commands are determined by checking the CommandManager
instance, passed in as the "default" value for the action.
"""
def __call__(self, parser, namespace, values, option_string=None):
outputs = []
max_len = 0
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands for API v1 :\n')
for name, ep in sorted(app.command_manager):
factory = ep.load()
cmd = factory(self, None)
one_liner = cmd.get_description().split('\n')[0]
outputs.append((name, one_liner))
max_len = max(len(name), max_len)
for (name, one_liner) in outputs:
app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner))
sys.exit(0)
class BashCompletionCommand(command.Command):
"""Prints all of the commands and options for bash-completion."""
def take_action(self, parsed_args):
commands = set()
options = set()
for option, _action in self.app.parser._option_string_actions.items():
options.add(option)
for command_name, _cmd in self.app.command_manager:
commands.add(command_name)
print(' '.join(commands | options))
class GlareShell(app.App):
def __init__(self):
super(GlareShell, self).__init__(
description=__doc__.strip(),
version=glareclient.__version__,
command_manager=commandmanager.CommandManager('glare.cli'),
)
self._set_shell_commands(self._get_commands())
def configure_logging(self):
log_lvl = logging.DEBUG if self.options.debug else logging.WARNING
logging.basicConfig(
format="%(levelname)s (%(module)s) %(message)s",
level=log_lvl
)
logging.getLogger('iso8601').setLevel(logging.WARNING)
if self.options.verbose_level <= 1:
logging.getLogger('requests').setLevel(logging.WARNING)
def build_option_parser(self, description, version,
argparse_kwargs=None):
"""Return an argparse option parser for this application.
Subclasses may override this method to extend
the parser with more global options.
:param description: full description of the application
:paramtype description: str
:param version: version number for the application
:paramtype version: str
:param argparse_kwargs: extra keyword argument passed to the
ArgumentParser constructor
:paramtype extra_kwargs: dict
"""
argparse_kwargs = argparse_kwargs or {}
parser = argparse.ArgumentParser(
description=description,
add_help=False,
formatter_class=OpenStackHelpFormatter,
**argparse_kwargs
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {0}'.format(version),
help='Show program\'s version number and exit.'
)
parser.add_argument(
'-v', '--verbose',
action='count',
dest='verbose_level',
default=self.DEFAULT_VERBOSE_LEVEL,
help='Increase verbosity of output. Can be repeated.',
)
parser.add_argument(
'--log-file',
action='store',
default=None,
help='Specify a file to log output. Disabled by default.',
)
parser.add_argument(
'-q', '--quiet',
action='store_const',
dest='verbose_level',
const=0,
help='Suppress output except warnings and errors.',
)
parser.add_argument(
'-h', '--help',
action=HelpAction,
nargs=0,
default=self, # tricky
help="Show this help message and exit.",
)
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='Show tracebacks on errors.',
)
parser.add_argument(
'--os-glare-url',
action='store',
dest='glare_url',
default=env('OS_GLARE_URL'),
help='Glare API host (Env: OS_GLARE_URL)'
)
parser.add_argument(
'--os-glare-version',
action='store',
dest='glare_version',
default=env('OS_GLARE_VERSION', default='v1'),
help='Glare API version (default = v1) (Env: '
'OS_GLARE_VERSION)'
)
parser.add_argument(
'--glare-url',
metavar='<GLARE_URL>',
default=utils.env('GLARE_URL'),
help='Glare endpoint url (Env: GLARE_URL)')
parser.add_argument(
'--keycloak-auth-url',
action='store',
dest='keycloak_auth_url',
default=utils.env('KEYCLOAK_AUTH_URL'),
help='Keycloak auth url (Env: KEYCLOAK_AUTH_URL)')
parser.add_argument(
'--openid-client-id',
action='store',
dest='openid_client_id',
default=utils.env('OPENID_CLIENT_ID') or 'admin-cli',
help='Client ID (according to OpenID Connect)'
' (Env: OPENID_CLIENT_ID)')
parser.add_argument(
'--auth-token',
action='store',
dest='auth_token',
default=utils.env('AUTH_TOKEN'),
help='Authentication token (Env: AUTH_TOKEN)')
parser.add_argument(
'--keycloak-realm-name',
action='store',
dest='keycloak_realm_name',
default=utils.env('KEYCLOAK_REALM_NAME'),
help='With keycloak glare auth type: Realm name to scope to'
' (Env: KEYCLOAK_REALM_NAME)')
parser.add_argument(
'--keycloak-username',
action='store',
dest='keycloak_username',
default=utils.env('KEYCLOAK_USERNAME'),
help='Keycloak username (Env: KEYCLOAK_USERNAME)')
parser.add_argument(
'--keycloak-password',
action='store',
dest='keycloak_password',
default=utils.env('KEYCLOAK_PASSWORD'),
help='Keycloak user password (Env: KEYCLOAK_PASSWORD)')
return parser
def initialize_app(self, argv):
self._clear_shell_commands()
self._set_shell_commands(self._get_commands())
do_help = ('help' in argv) or ('-h' in argv) or not argv
# Set default for auth_url if not supplied. The default is not
# set at the parser to support use cases where auth is not enabled.
# An example use case would be a developer's environment.
# bash-completion should not require authentification.
if do_help or ('bash-completion' in argv):
self.options.auth_url = None
self.client = client.Client(
endpoint=self.options.glare_url,
auth_token=self.options.auth_token,
keycloak_auth_url=self.options.keycloak_auth_url,
openid_client_id=self.options.openid_client_id,
keycloak_realm_name=self.options.keycloak_realm_name,
keycloak_username=self.options.keycloak_username,
keycloak_password=self.options.keycloak_password,
)
# Adding client_manager variable to make glare client work with
# unified OpenStack client.
ClientManager = type(
'ClientManager',
(object,),
dict(artifact=self.client)
)
self.client_manager = ClientManager()
def _set_shell_commands(self, cmds_dict):
for k, v in cmds_dict.items():
self.command_manager.add_command(k, v)
def _clear_shell_commands(self):
exclude_cmds = ['help', 'complete']
cmds = self.command_manager.commands.copy()
for k, v in cmds.items():
if k not in exclude_cmds:
self.command_manager.commands.pop(k)
@staticmethod
def _get_commands():
return {
'bash-completion': BashCompletionCommand,
'list': glareclient.osc.v1.artifacts.ListArtifacts,
'show': glareclient.osc.v1.artifacts.ShowArtifact,
'create': glareclient.osc.v1.artifacts.CreateArtifact,
'delete': glareclient.osc.v1.artifacts.DeleteArtifact,
'update': glareclient.osc.v1.artifacts.UpdateArtifact,
'deactivate': glareclient.osc.v1.artifacts.DeactivateArtifact,
'reactivate': glareclient.osc.v1.artifacts.ReactivateArtifact,
'publish': glareclient.osc.v1.artifacts.PublishArtifact,
'add-tag': glareclient.osc.v1.artifacts.AddTag,
'remove-tag': glareclient.osc.v1.artifacts.RemoveTag,
'upload': glareclient.osc.v1.blobs.UploadBlob,
'download': glareclient.osc.v1.blobs.DownloadBlob,
'location': glareclient.osc.v1.blobs.AddLocation
}
def main(argv=sys.argv[1:]):
return GlareShell().run(argv)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@ -25,10 +25,13 @@ class TestArtifactPlugin(base.TestCaseShell):
instance._api_version = {"artifact": '1'}
instance._region_name = 'glare_region'
instance.session = 'glare_session'
instance.get_configuration.return_value = {
'glare_url': 'http://example.com:9494/',
'keycloak_auth_url': None}
plugin.make_client(instance)
p_client.assert_called_with(
mock.ANY,
'http://example.com:9494/',
region_name='glare_region',
session='glare_session',
service_type='artifact')

View File

@ -104,7 +104,7 @@ class HttpClientTest(testtools.TestCase):
headers={'X-Region-Name': 'RegionOne',
'User-Agent': 'python-glareclient'})
def test_http_json_request(self, mock_request):
def test_http_process_request(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
@ -112,7 +112,7 @@ class HttpClientTest(testtools.TestCase):
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
resp, body = client.process_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
@ -121,7 +121,8 @@ class HttpClientTest(testtools.TestCase):
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request):
def test_http_process_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 = \
@ -135,7 +136,7 @@ class HttpClientTest(testtools.TestCase):
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')
resp, body = client.process_request('', 'GET', data='text')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
@ -148,7 +149,7 @@ class HttpClientTest(testtools.TestCase):
headers={'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-glareclient'})
def test_http_json_request_w_req_body(self, mock_request):
def test_http_process_request_w_req_body(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
@ -157,7 +158,7 @@ class HttpClientTest(testtools.TestCase):
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET', data='test-body')
resp, body = client.process_request('', 'GET', data='test-body')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
@ -166,7 +167,8 @@ class HttpClientTest(testtools.TestCase):
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
def test_http_process_request_non_json_resp_cont_type(
self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
@ -175,14 +177,14 @@ class HttpClientTest(testtools.TestCase):
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET', data='test-data')
resp, body = client.process_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,
headers={'User-Agent': 'python-glareclient'})
def test_http_json_request_invalid_json(self, mock_request):
def test_http_process_request_invalid_json(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
@ -191,7 +193,7 @@ class HttpClientTest(testtools.TestCase):
'invalid-json')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
resp, body = client.process_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertIsNone(body)
mock_request.assert_called_once_with(
@ -211,7 +213,7 @@ class HttpClientTest(testtools.TestCase):
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'DELETE')
resp, body = client.process_request('', 'DELETE')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
@ -235,7 +237,7 @@ class HttpClientTest(testtools.TestCase):
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'POST', json={})
resp, body = client.process_request('', 'POST', json={})
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
@ -261,7 +263,7 @@ class HttpClientTest(testtools.TestCase):
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'PUT', json={})
resp, body = client.process_request('', 'PUT', json={})
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
@ -283,7 +285,7 @@ class HttpClientTest(testtools.TestCase):
'')
client = http.HTTPClient('http://example.com:9494/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
client.process_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo',
allow_redirects=False,
@ -297,13 +299,13 @@ class HttpClientTest(testtools.TestCase):
'')
client = http.HTTPClient('http://example.com:9494/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
client.process_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo',
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'})
def test_http_json_request_redirect(self, mock_request):
def test_http_process_request_redirect(self, mock_request):
# Record the 302
mock_request.side_effect = [
fakes.FakeHTTPResponse(
@ -316,7 +318,7 @@ class HttpClientTest(testtools.TestCase):
'{}')]
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
resp, body = client.process_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
@ -329,14 +331,15 @@ class HttpClientTest(testtools.TestCase):
headers={'User-Agent': 'python-glareclient'})
])
def test_http_404_json_request(self, mock_request):
def test_http_404_process_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')
e = self.assertRaises(exc.HTTPNotFound, client.process_request,
'', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 404
@ -345,14 +348,14 @@ class HttpClientTest(testtools.TestCase):
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'})
def test_http_300_json_request(self, mock_request):
def test_http_300_process_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')
exc.HTTPMultipleChoices, client.process_request, '', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
@ -362,7 +365,7 @@ class HttpClientTest(testtools.TestCase):
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'})
def test_fake_json_request(self, mock_request):
def test_fake_process_request(self, mock_request):
headers = {'User-Agent': 'python-glareclient'}
mock_request.side_effect = [socket.gaierror]
@ -403,7 +406,7 @@ class HttpClientTest(testtools.TestCase):
'{}')
client = http.HTTPClient('http://example.com:9494', timeout='123')
resp, body = client.json_request('', 'GET')
resp, body = client.process_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(

View File

@ -31,6 +31,9 @@ setup-hooks =
pbr.hooks.setup_hook
[entry_points]
console_scripts =
glare = glareclient.shell:main
openstack.cli.extension =
artifact = glareclient.osc.plugin