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. # limitations under the License.
from oslo.config import cfg
import six import six
from marconiclient.auth import base from marconiclient.auth import base
@ -34,18 +33,18 @@ if keystone:
_BACKENDS['keystone'] = keystone.KeystoneAuth _BACKENDS['keystone'] = keystone.KeystoneAuth
def _register_opts(conf): def get_backend(backend='noauth', options=None):
"""Registers auth cli options. """Loads backend `auth_backend`
This function exists mostly for testing :params backend: The backend name to load.
purposes. 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', if options is None:
help='Backend plugin to use for authentication') options = {}
conf.register_cli_opt(backend_opt)
backend = _BACKENDS[backend](options)
def get_backend(conf):
_register_opts(conf)
backend = _BACKENDS[conf.auth_backend](conf)
return backend return backend

View File

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

View File

@ -15,40 +15,39 @@
import uuid import uuid
from oslo.config import cfg
from marconiclient.queues.v1 import queues from marconiclient.queues.v1 import queues
from marconiclient import transport 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): class Client(object):
"""Client base class
def __init__(self, conf, url=None, version=1): :param url: Marconi's instance base url.
self.conf = conf :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 def __init__(self, url=None, version=1, conf=None):
# the CLI options until the class is instantiated self.conf = conf or {}
# which is dumb. It'll refactored when the CLI API
# work starts. self.api_url = url
self.conf.register_cli_opts(_CLIENT_OPTIONS)
self.api_url = self.conf.os_queues_url or url
self.api_version = version self.api_version = version
self.auth_opts = self.conf.get('auth_opts', {})
self.client_uuid = self.conf.client_uuid self.client_uuid = self.conf.get('client_uuid',
uuid.uuid4().hex)
def transport(self): def transport(self):
"""Gets a transport based on conf.""" """Gets a transport based the api url and version."""
return transport.get_transport_for_conf(self.conf) return transport.get_transport_for(self.url,
self.api_version)
def queue(self, ref, **kwargs): def queue(self, ref, **kwargs):
"""Returns a queue instance """Returns a queue instance

View File

@ -43,12 +43,13 @@ class Queue(object):
:type request: `transport.request.Request` :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) return (trans or self.client.transport)
def _request_and_transport(self): def _request_and_transport(self):
api = 'queues.v' + str(self.client.api_version) 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, endpoint=self.client.api_url,
api=api) api=api)

View File

@ -16,15 +16,13 @@
import fixtures import fixtures
import testtools import testtools
from oslo.config import cfg
class TestBase(testtools.TestCase): class TestBase(testtools.TestCase):
def setUp(self): def setUp(self):
super(TestBase, self).setUp() super(TestBase, self).setUp()
self.conf = cfg.ConfigOpts() self.conf = {}
self.useFixture(fixtures.FakeLogger('marconi')) self.useFixture(fixtures.FakeLogger('marconi'))
# NOTE(kgriffs): Don't monkey-patch stdout since it breaks # 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 If a group argument is supplied, the overrides are applied to
the specified configuration option group. 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(): parent = (group and self.conf.setdefault(group, {})
self.conf.set_override(k, v, group) or self.conf)
parent.update(kw)

View File

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

View File

@ -13,29 +13,21 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from oslo.config import cfg
import six import six
from six.moves.urllib import parse from six.moves.urllib import parse
from stevedore import driver from stevedore import driver
from marconiclient import errors as _errors 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(transport='http', version=1, options=None):
def get_transport(conf, transport, version=1):
"""Gets a transport and returns it. """Gets a transport and returns it.
:param conf: the user configuration :param transport: Transport name.
:type conf: cfg.ConfigOpts Default: http
:param transport: Transport name
:type transport: `six.string_types` :type transport: `six.string_types`
:param version: Version of the target transport. :param version: Version of the target transport.
Default: 1
:type version: int :type version: int
:returns: A `Transport` instance. :returns: A `Transport` instance.
@ -49,36 +41,20 @@ def get_transport(conf, transport, version=1):
mgr = driver.DriverManager(namespace, mgr = driver.DriverManager(namespace,
entry_point, entry_point,
invoke_on_load=True, invoke_on_load=True,
invoke_args=[conf]) invoke_args=[options])
except RuntimeError as ex: except RuntimeError as ex:
raise _errors.DriverLoadFailure(entry_point, ex) raise _errors.DriverLoadFailure(entry_point, ex)
return mgr.driver return mgr.driver
def get_transport_for_conf(conf): def get_transport_for(url_or_request, version=1, options=None):
"""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):
"""Gets a transport for a given url. """Gets a transport for a given url.
An example transport URL might be:: An example transport URL might be::
zmq://example.org:8888/v1/ zmq://example.org:8888/v1/
:param conf: the user configuration
:type conf: cfg.ConfigOpts
:param url_or_request: a transport URL :param url_or_request: a transport URL
:type url_or_request: `six.string_types` or :type url_or_request: `six.string_types` or
`marconiclient.transport.request.Request` `marconiclient.transport.request.Request`
@ -94,4 +70,4 @@ def get_transport_for(conf, url_or_request, version=1):
url = url_or_request.endpoint url = url_or_request.endpoint
parsed = parse.urlparse(url) 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) @six.add_metaclass(abc.ABCMeta)
class Transport(object): class Transport(object):
def __init__(self, conf): def __init__(self, options):
self.conf = conf self.options = options
@abc.abstractmethod @abc.abstractmethod
def send(self, request): def send(self, request):

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@
import mock import mock
from oslo.config import cfg
try: try:
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
@ -42,13 +41,8 @@ class TestKeystoneAuth(base.TestBase):
if not ksclient: if not ksclient:
self.skipTest('Keystone client is not installed') self.skipTest('Keystone client is not installed')
auth._register_opts(self.conf) self.auth = auth.get_backend(backend='keystone',
self.config(auth_backend='keystone') options=self.conf)
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))
def test_no_token(self): def test_no_token(self):
test_endpoint = 'http://example.org:8888' test_endpoint = 'http://example.org:8888'