Remove the dependency on oslo.config

Client libraries shouldn't depend on specific configuration libraries,
since that will make it difficult for other applications to adopt the
library.

This patch replaces all oslo.config usages with dictionaries. The client
will use those as options holders and all parameters are considered
optional.

Notice that `options` was preferred instead of `**options` since this
dictionary is considered a 'config object' and not a set of optional
parameters. This will make it easier for applications using
marconiclient to hold a single, mutable instance of options to pass
around.

Partially-Implements blueprint python-marconiclient-v1

Change-Id: Ib41f2957689545f05dab19338842fd9a953f2f76
This commit is contained in:
Flavio Percoco 2013-11-19 16:49:16 +01:00
parent 664c366bd8
commit d1a0c1f405
12 changed files with 95 additions and 160 deletions

View File

@ -14,7 +14,6 @@
# limitations under the License.
from oslo.config import cfg
import six
from marconiclient.auth import base
@ -34,18 +33,18 @@ if keystone:
_BACKENDS['keystone'] = keystone.KeystoneAuth
def _register_opts(conf):
"""Registers auth cli options.
def get_backend(backend='noauth', options=None):
"""Loads backend `auth_backend`
This function exists mostly for testing
purposes.
:params backend: The backend name to load.
Default: `noauth`
:type backend: `six.string_types`
:param options: Options to pass to the Auth
backend. Refer to the backend for more info.
:type options: `dict`
"""
backend_opt = cfg.StrOpt('auth_backend', default='noauth',
help='Backend plugin to use for authentication')
conf.register_cli_opt(backend_opt)
if options is None:
options = {}
def get_backend(conf):
_register_opts(conf)
backend = _BACKENDS[conf.auth_backend](conf)
backend = _BACKENDS[backend](options)
return backend

View File

@ -13,55 +13,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo.config import cfg
from keystoneclient.v2_0 import client as ksclient
from marconiclient.auth import base
from marconiclient.common import utils
# NOTE(flaper87): Some of the code below
# was brought to you by the very unique
# work of ceilometerclient.
class KeystoneAuth(base.AuthBackend):
"""Keystone Auth backend
_CLI_OPTIONS = [
cfg.StrOpt("os_username", default=utils.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME]'),
cfg.StrOpt("os_password", default=utils.env('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD]'),
cfg.StrOpt("os_project_id", default=utils.env('OS_PROJECT_ID'),
help='Defaults to env[OS_PROJECT_ID]'),
cfg.StrOpt("os_project_name", default=utils.env('OS_PROJECT_NAME'),
help='Defaults to env[OS_PROJECT_NAME]'),
cfg.StrOpt("os_auth_url", default=utils.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL]'),
cfg.StrOpt("os_auth_token", default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN]'),
cfg.StrOpt("os_region_name", default=utils.env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME]'),
cfg.StrOpt("os_service_type", default=utils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE]'),
cfg.StrOpt("os_service_type", default=utils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE]'),
cfg.StrOpt("os_endpoint_type", default=utils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE]'),
]
def __init__(self, conf):
super(KeystoneAuth, self).__init__(conf)
conf.register_cli_opts(self._CLI_OPTIONS)
:params conf: A dictionary with Keystone's
custom parameters:
- os_username
- os_password
- os_project_id
- os_project_name
- os_auth_url
- os_auth_token
- os_region_name
- os_service_type
- os_service_type
- os_endpoint_type
:type conf: `dict`
"""
def _get_ksclient(self, **kwargs):
"""Get an endpoint and auth token from Keystone.
@ -73,18 +49,11 @@ class KeystoneAuth(base.AuthBackend):
* insecure: allow insecure SSL (no cert verification)
* project_{name|id}: name or ID of project
"""
return ksclient.Client(username=self.conf.os_username,
password=self.conf.os_password,
tenant_id=self.conf.os_project_id,
tenant_name=self.conf.os_project_name,
auth_url=self.conf.os_auth_url,
insecure=self.conf.insecure)
return ksclient.Client(**kwargs)
def _get_endpoint(self, client):
def _get_endpoint(self, client, **extra):
"""Get an endpoint using the provided keystone client."""
return client.service_catalog.url_for(
service_type=self.conf.service_type or 'queuing',
endpoint_type=self.conf.endpoint_type or 'publicURL')
return client.service_catalog.url_for(**extra)
def authenticate(self, api_version, request):
"""Get an authtenticated client, based on the credentials
@ -95,21 +64,19 @@ class KeystoneAuth(base.AuthBackend):
the auth information.
"""
token = self.conf.os_auth_token
if not self.conf.os_auth_token or not request.endpoint:
token = self.conf.get('os_auth_token')
if not token or not request.endpoint:
# NOTE(flaper87): Lets assume all the
# required information was provided
# either through env variables or CLI
# params. Let keystoneclient fail otherwise.
ks_kwargs = {
'username': self.conf.os_username,
'password': self.conf.os_password,
'tenant_id': self.conf.os_project_id,
'tenant_name': self.conf.os_project_name,
'auth_url': self.conf.os_auth_url,
'service_type': self.conf.os_service_type,
'endpoint_type': self.conf.os_endpoint_type,
'insecure': self.conf.insecure,
'username': self.conf.get('os_username'),
'password': self.conf.get('os_password'),
'tenant_id': self.conf.get('os_project_id'),
'tenant_name': self.conf.get('os_project_name'),
'auth_url': self.conf.get('os_auth_url'),
'insecure': self.conf.get('insecure'),
}
_ksclient = self._get_ksclient(**ks_kwargs)
@ -118,7 +85,13 @@ class KeystoneAuth(base.AuthBackend):
token = _ksclient.auth_token
if not request.endpoint:
request.endpoint = self._get_endpoint(_ksclient, **ks_kwargs)
extra = {
'service_type': self.conf.get('os_service_type',
'queuing'),
'endpoint_type': self.conf.get('os_endpoint_type',
'publicURL'),
}
request.endpoint = self._get_endpoint(_ksclient, **extra)
# NOTE(flaper87): Update the request spec
# with the final token.

View File

@ -15,40 +15,39 @@
import uuid
from oslo.config import cfg
from marconiclient.queues.v1 import queues
from marconiclient import transport
_CLIENT_OPTIONS = [
cfg.StrOpt('os_queues_url',
help='Queues remote URL'),
cfg.StrOpt('client_uuid',
default=uuid.uuid4().hex,
help='Client UUID'),
]
class Client(object):
"""Client base class
def __init__(self, conf, url=None, version=1):
self.conf = conf
:param url: Marconi's instance base url.
:type url: `six.text_type`
:param version: API Version pointing to.
:type version: `int`
:param options: Extra options:
- client_uuid: Custom client uuid. A new one
will be generated, if not passed.
- auth_opts: Authentication options:
- backend
- options
:type options: `dict`
"""
# NOTE(flaper87): This won't actually register
# the CLI options until the class is instantiated
# which is dumb. It'll refactored when the CLI API
# work starts.
self.conf.register_cli_opts(_CLIENT_OPTIONS)
self.api_url = self.conf.os_queues_url or url
def __init__(self, url=None, version=1, conf=None):
self.conf = conf or {}
self.api_url = url
self.api_version = version
self.client_uuid = self.conf.client_uuid
self.auth_opts = self.conf.get('auth_opts', {})
self.client_uuid = self.conf.get('client_uuid',
uuid.uuid4().hex)
def transport(self):
"""Gets a transport based on conf."""
return transport.get_transport_for_conf(self.conf)
"""Gets a transport based the api url and version."""
return transport.get_transport_for(self.url,
self.api_version)
def queue(self, ref, **kwargs):
"""Returns a queue instance

View File

@ -43,12 +43,13 @@ class Queue(object):
:type request: `transport.request.Request`
"""
trans = transport.get_transport_for(self.client.conf, request)
trans = transport.get_transport_for(request,
options=self.client.conf)
return (trans or self.client.transport)
def _request_and_transport(self):
api = 'queues.v' + str(self.client.api_version)
req = request.prepare_request(self.client.conf,
req = request.prepare_request(self.client.auth_opts,
endpoint=self.client.api_url,
api=api)

View File

@ -16,15 +16,13 @@
import fixtures
import testtools
from oslo.config import cfg
class TestBase(testtools.TestCase):
def setUp(self):
super(TestBase, self).setUp()
self.conf = cfg.ConfigOpts()
self.conf = {}
self.useFixture(fixtures.FakeLogger('marconi'))
# NOTE(kgriffs): Don't monkey-patch stdout since it breaks
@ -40,9 +38,7 @@ class TestBase(testtools.TestCase):
If a group argument is supplied, the overrides are applied to
the specified configuration option group.
All overrides are automatically cleared at the end of the current
test by the tearDown() method.
"""
for k, v in kw.items():
self.conf.set_override(k, v, group)
parent = (group and self.conf.setdefault(group, {})
or self.conf)
parent.update(kw)

View File

@ -42,8 +42,8 @@ class QueuesV1QueueTestBase(base.TestBase):
super(QueuesV1QueueTestBase, self).setUp()
self.transport = self.transport_cls(self.conf)
self.client = client.Client(self.conf, self.url,
self.version)
self.client = client.Client(self.url, self.version,
self.conf)
# NOTE(flaper87): Nasty monkeypatch, lets use
# the dummy transport here.

View File

@ -13,29 +13,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo.config import cfg
import six
from six.moves.urllib import parse
from stevedore import driver
from marconiclient import errors as _errors
_TRANSPORT_OPTIONS = [
cfg.StrOpt('default_transport', default='http',
help='Transport to use as default'),
cfg.IntOpt('default_transport_version', default=1,
help='Transport to use as default'),
]
def get_transport(conf, transport, version=1):
def get_transport(transport='http', version=1, options=None):
"""Gets a transport and returns it.
:param conf: the user configuration
:type conf: cfg.ConfigOpts
:param transport: Transport name
:param transport: Transport name.
Default: http
:type transport: `six.string_types`
:param version: Version of the target transport.
Default: 1
:type version: int
:returns: A `Transport` instance.
@ -49,36 +41,20 @@ def get_transport(conf, transport, version=1):
mgr = driver.DriverManager(namespace,
entry_point,
invoke_on_load=True,
invoke_args=[conf])
invoke_args=[options])
except RuntimeError as ex:
raise _errors.DriverLoadFailure(entry_point, ex)
return mgr.driver
def get_transport_for_conf(conf):
"""Gets a transport based on the config object
It'll load a transport based on the `default-transport`
and `default-transport-version` params.
:param conf: the user configuration
:type conf: cfg.ConfigOpts
"""
conf.register_opts(_TRANSPORT_OPTIONS)
return get_transport(conf, conf.default_transport,
conf.default_transport_version)
def get_transport_for(conf, url_or_request, version=1):
def get_transport_for(url_or_request, version=1, options=None):
"""Gets a transport for a given url.
An example transport URL might be::
zmq://example.org:8888/v1/
:param conf: the user configuration
:type conf: cfg.ConfigOpts
:param url_or_request: a transport URL
:type url_or_request: `six.string_types` or
`marconiclient.transport.request.Request`
@ -94,4 +70,4 @@ def get_transport_for(conf, url_or_request, version=1):
url = url_or_request.endpoint
parsed = parse.urlparse(url)
return get_transport(conf, parsed.scheme, version)
return get_transport(parsed.scheme, version, options)

View File

@ -21,8 +21,8 @@ import six
@six.add_metaclass(abc.ABCMeta)
class Transport(object):
def __init__(self, conf):
self.conf = conf
def __init__(self, options):
self.options = options
@abc.abstractmethod
def send(self, request):

View File

@ -32,8 +32,8 @@ class HttpTransport(base.Transport):
404: errors.ResourceNotFound,
}
def __init__(self, conf):
super(HttpTransport, self).__init__(conf)
def __init__(self, options):
super(HttpTransport, self).__init__(options)
self.client = http.Client()
def _prepare(self, request):

View File

@ -21,7 +21,7 @@ from marconiclient import auth
from marconiclient import errors
def prepare_request(conf, data=None, **kwargs):
def prepare_request(auth_opts=None, data=None, **kwargs):
"""Prepares a request
This method takes care of authentication
@ -30,8 +30,8 @@ def prepare_request(conf, data=None, **kwargs):
The request returned by this call is ready to
be sent to the server.
:param conf: `cfg.ConfigOpts` instance to use.
:type conf: `cfg.ConfigOpts`
:param auth_opts: Auth parameters
:type auth_opts: `dict`
:param data: Optional data to send along with the
request. If data is not None, it'll be serialized.
:type data: Any primitive type that is json-serializable.
@ -42,7 +42,7 @@ def prepare_request(conf, data=None, **kwargs):
"""
req = Request(**kwargs)
auth_backend = auth.get_backend(conf)
auth_backend = auth.get_backend(**(auth_opts or {}))
# TODO(flaper87): Do something smarter
# to get the api_version.
req = auth_backend.authenticate(1, req)

View File

@ -21,16 +21,13 @@ class TestBaseAuth(base.TestBase):
def test_get_backend(self):
try:
auth.get_backend(self.conf)
auth.get_backend(options=self.conf)
except KeyError:
self.fail("Test failed")
def test_get_non_existing_backend(self):
auth._register_opts(self.conf)
self.config(auth_backend="not_existing")
try:
auth.get_backend(self.conf)
auth.get_backend('not_existing')
self.fail("Test failed")
except KeyError:
pass

View File

@ -15,7 +15,6 @@
import mock
from oslo.config import cfg
try:
from keystoneclient.v2_0 import client as ksclient
@ -42,13 +41,8 @@ class TestKeystoneAuth(base.TestBase):
if not ksclient:
self.skipTest('Keystone client is not installed')
auth._register_opts(self.conf)
self.config(auth_backend='keystone')
self.auth = auth.get_backend(self.conf)
# FIXME(flaper87): Remove once insecure is added
# in the right place.
self.conf.register_opt(cfg.BoolOpt('insecure', default=False))
self.auth = auth.get_backend(backend='keystone',
options=self.conf)
def test_no_token(self):
test_endpoint = 'http://example.org:8888'