Add keycloak auth support
Change-Id: I88c79656fbc6cd9c055569979083ef385ba84563
This commit is contained in:
parent
e7e555e045
commit
36f0e7318d
|
@ -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 '
|
||||
|
|
|
@ -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']
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:]))
|
|
@ -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')
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue