Plugin loading from config objects
Provide a pattern for auth plugins to load themselves from a config object. The first user of this will be auth_token middleware however it is not likely to be the only user. By doing this in an exportable way we are defining a single config file format for specifying how to load a plugin for all services. We also provide a standard way of retrieving a plugins options for loading via other mechanisms. Blueprint: standard-client-params Change-Id: I353b26a1ffc04a20666e76f5bd2f1e6d7c19a22d
This commit is contained in:
parent
3e88c35cd7
commit
5c91ede447
|
@ -13,6 +13,31 @@
|
|||
import abc
|
||||
|
||||
import six
|
||||
import stevedore
|
||||
|
||||
from keystoneclient import exceptions
|
||||
|
||||
PLUGIN_NAMESPACE = 'keystoneclient.auth.plugin'
|
||||
|
||||
|
||||
def get_plugin_class(name):
|
||||
"""Retrieve a plugin class by its entrypoint name.
|
||||
|
||||
:param str name: The name of the object to get.
|
||||
|
||||
:returns: An auth plugin class.
|
||||
|
||||
:raises exceptions.NoMatchingPlugin: if a plugin cannot be created.
|
||||
"""
|
||||
try:
|
||||
mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE,
|
||||
name=name,
|
||||
invoke_on_load=False)
|
||||
except RuntimeError:
|
||||
msg = 'The plugin %s could not be found' % name
|
||||
raise exceptions.NoMatchingPlugin(msg)
|
||||
|
||||
return mgr.driver
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
|
@ -70,3 +95,24 @@ class BaseAuthPlugin(object):
|
|||
If nothing happens returns False to indicate give up.
|
||||
"""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
"""Return the list of parameters associated with the auth plugin.
|
||||
|
||||
This list may be used to generate CLI or config arguments.
|
||||
|
||||
:returns list: A list of Param objects describing available plugin
|
||||
parameters.
|
||||
"""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def load_from_options(cls, **kwargs):
|
||||
"""Create a plugin from the arguments retrieved from get_options.
|
||||
|
||||
A client can override this function to do argument validation or to
|
||||
handle differences between the registered options and what is required
|
||||
to create the plugin.
|
||||
"""
|
||||
return cls(**kwargs)
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
# 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.config import cfg
|
||||
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient import exceptions
|
||||
|
||||
_AUTH_PLUGIN_OPT = cfg.StrOpt('auth_plugin', help='Name of the plugin to load')
|
||||
|
||||
_section_help = 'Config Section from which to load plugin specific options'
|
||||
_AUTH_SECTION_OPT = cfg.StrOpt('auth_section', help=_section_help)
|
||||
|
||||
|
||||
def get_common_conf_options():
|
||||
"""Get the oslo.config options common for all auth plugins.
|
||||
|
||||
These may be useful without being registered for config file generation
|
||||
or to manipulate the options before registering them yourself.
|
||||
|
||||
The options that are set are:
|
||||
:auth_plugin: The name of the pluign to load.
|
||||
:auth_section: The config file section to load options from.
|
||||
|
||||
:returns: A list of oslo.config options.
|
||||
"""
|
||||
return [_AUTH_PLUGIN_OPT, _AUTH_SECTION_OPT]
|
||||
|
||||
|
||||
def get_plugin_options(name):
|
||||
"""Get the oslo.config options for a specific plugin.
|
||||
|
||||
This will be the list of config options that is registered and loaded by
|
||||
the specified plugin.
|
||||
|
||||
:returns: A list of oslo.config options.
|
||||
"""
|
||||
return base.get_plugin_class(name).get_options()
|
||||
|
||||
|
||||
def register_conf_options(conf, group):
|
||||
"""Register the oslo.config options that are needed for a plugin.
|
||||
|
||||
This only registers the basic options shared by all plugins. Options that
|
||||
are specific to a plugin are loaded just before they are read.
|
||||
|
||||
The defined options are:
|
||||
|
||||
- auth_plugin: the name of the auth plugin that will be used for
|
||||
authentication.
|
||||
- auth_section: the group from which further auth plugin options should be
|
||||
taken. If section is not provided then the auth plugin options will be
|
||||
taken from the same group as provided in the parameters.
|
||||
|
||||
:param oslo.config.Cfg conf: config object to register with.
|
||||
:param string group: The ini group to register options in.
|
||||
"""
|
||||
conf.register_opt(_AUTH_SECTION_OPT, group=group)
|
||||
|
||||
# NOTE(jamielennox): plugins are allowed to specify a 'section' which is
|
||||
# the group that auth options should be taken from. If not present they
|
||||
# come from the same as the base options were registered in. If present
|
||||
# then the auth_plugin option may be read from that section so add that
|
||||
# option.
|
||||
if conf[group].auth_section:
|
||||
group = conf[group].auth_section
|
||||
|
||||
conf.register_opt(_AUTH_PLUGIN_OPT, group=group)
|
||||
|
||||
|
||||
def load_from_conf_options(conf, group, **kwargs):
|
||||
"""Load a plugin from an oslo.config CONF object.
|
||||
|
||||
Each plugin will register there own required options and so there is no
|
||||
standard list and the plugin should be consulted.
|
||||
|
||||
The base options should have been registered with register_conf_options
|
||||
before this function is called.
|
||||
|
||||
:param conf: An oslo.config conf object.
|
||||
:param string group: The group name that options should be read from.
|
||||
|
||||
:returns plugin: An authentication Plugin.
|
||||
|
||||
:raises exceptions.NoMatchingPlugin: if a plugin cannot be created.
|
||||
"""
|
||||
# NOTE(jamielennox): plugins are allowed to specify a 'section' which is
|
||||
# the group that auth options should be taken from. If not present they
|
||||
# come from the same as the base options were registered in.
|
||||
if conf[group].auth_section:
|
||||
group = conf[group].auth_section
|
||||
|
||||
name = conf[group].auth_plugin
|
||||
if not name:
|
||||
raise exceptions.NoMatchingPlugin('No plugin name provided for config')
|
||||
|
||||
plugin_class = base.get_plugin_class(name)
|
||||
plugin_opts = plugin_class.get_options()
|
||||
conf.register_opts(plugin_opts, group=group)
|
||||
|
||||
for opt in plugin_opts:
|
||||
val = conf[group][opt.dest]
|
||||
if val is not None:
|
||||
val = opt.type(val)
|
||||
kwargs.setdefault(opt.dest, val)
|
||||
|
||||
return plugin_class.load_from_options(**kwargs)
|
|
@ -13,6 +13,7 @@
|
|||
import abc
|
||||
import logging
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from keystoneclient import _discover
|
||||
|
@ -178,3 +179,13 @@ class BaseIdentityPlugin(base.BaseAuthPlugin):
|
|||
session_endpoint_cache[sc_url] = disc
|
||||
|
||||
return disc.url_for(version)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(BaseIdentityPlugin, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('auth-url', help='Authentication URL'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import abc
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from keystoneclient import access
|
||||
|
@ -23,6 +24,18 @@ from keystoneclient import utils
|
|||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Auth(base.BaseIdentityPlugin):
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Auth, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('tenant-id', help='Tenant ID'),
|
||||
cfg.StrOpt('tenant-name', help='Tenant Name'),
|
||||
cfg.StrOpt('trust-id', help='Trust ID'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
@utils.positional()
|
||||
def __init__(self, auth_url,
|
||||
trust_id=None,
|
||||
|
@ -90,6 +103,20 @@ class Password(Auth):
|
|||
return {'passwordCredentials': {'username': self.username,
|
||||
'password': self.password}}
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Password, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('user-name',
|
||||
dest='username',
|
||||
deprecated_name='username',
|
||||
help='Username to login with'),
|
||||
cfg.StrOpt('password', help='Password to use'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class Token(Auth):
|
||||
|
||||
|
@ -106,3 +133,13 @@ class Token(Auth):
|
|||
if headers is not None:
|
||||
headers['X-Auth-Token'] = self.token
|
||||
return {'token': {'id': self.token}}
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Token, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('token', help='Token'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import abc
|
||||
import logging
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from keystoneclient import access
|
||||
|
@ -115,6 +116,24 @@ class Auth(base.BaseIdentityPlugin):
|
|||
return access.AccessInfoV3(resp.headers['X-Subject-Token'],
|
||||
**resp_data)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Auth, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('domain-id', help='Domain ID to scope to'),
|
||||
cfg.StrOpt('domain-name', help='Domain name to scope to'),
|
||||
cfg.StrOpt('project-id', help='Project ID to scope to'),
|
||||
cfg.StrOpt('project-name', help='Project name to scope to'),
|
||||
cfg.StrOpt('project-domain-id',
|
||||
help='Domain ID containing project'),
|
||||
cfg.StrOpt('project-domain-name',
|
||||
help='Domain name containing project'),
|
||||
cfg.StrOpt('trust-id', help='Trust ID'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AuthMethod(object):
|
||||
|
@ -214,6 +233,21 @@ class PasswordMethod(AuthMethod):
|
|||
class Password(AuthConstructor):
|
||||
_auth_method_class = PasswordMethod
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Password, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('user-id', help='User ID'),
|
||||
cfg.StrOpt('user-name', dest='username', help='Username',
|
||||
deprecated_name='username'),
|
||||
cfg.StrOpt('user-domain-id', help="User's domain id"),
|
||||
cfg.StrOpt('user-domain-name', help="User's domain name"),
|
||||
cfg.StrOpt('password', help="User's password"),
|
||||
])
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class TokenMethod(AuthMethod):
|
||||
|
||||
|
@ -236,3 +270,13 @@ class Token(AuthConstructor):
|
|||
|
||||
def __init__(self, auth_url, token, **kwargs):
|
||||
super(Token, self).__init__(auth_url, token=token, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
options = super(Token, cls).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('token', help='Token to authenticate with'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
|
|
@ -37,3 +37,14 @@ class Token(base.BaseAuthPlugin):
|
|||
parameters passed to the plugin.
|
||||
"""
|
||||
return self.endpoint
|
||||
|
||||
def get_options(self):
|
||||
options = super(Token, self).get_options()
|
||||
|
||||
options.extend([
|
||||
cfg.StrOpt('endpoint',
|
||||
help='The endpoint that will always be used'),
|
||||
cfg.StrOpt('token', help='The token that will always be used'),
|
||||
])
|
||||
|
||||
return options
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
# 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 uuid
|
||||
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
import stevedore
|
||||
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient.auth import conf
|
||||
from keystoneclient.auth.identity import v2 as v2_auth
|
||||
from keystoneclient.auth.identity import v3 as v3_auth
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient.openstack.common.fixture import config
|
||||
from keystoneclient.tests.auth import utils
|
||||
|
||||
|
||||
class ConfTests(utils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ConfTests, self).setUp()
|
||||
self.conf_fixture = self.useFixture(config.Config())
|
||||
|
||||
# NOTE(jamielennox): we register the basic config options first because
|
||||
# we need them in place before we can stub them. We will need to run
|
||||
# the register again after we stub the auth section and auth plugin so
|
||||
# it can load the plugin specific options.
|
||||
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||
|
||||
def test_loading_v2(self):
|
||||
section = uuid.uuid4().hex
|
||||
username = uuid.uuid4().hex
|
||||
password = uuid.uuid4().hex
|
||||
trust_id = uuid.uuid4().hex
|
||||
tenant_id = uuid.uuid4().hex
|
||||
|
||||
self.conf_fixture.config(auth_section=section, group=self.GROUP)
|
||||
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||
|
||||
self.conf_fixture.register_opts(v2_auth.Password.get_options(),
|
||||
group=section)
|
||||
|
||||
self.conf_fixture.config(auth_plugin=self.V2PASS,
|
||||
username=username,
|
||||
password=password,
|
||||
trust_id=trust_id,
|
||||
tenant_id=tenant_id,
|
||||
group=section)
|
||||
|
||||
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||
|
||||
self.assertEqual(username, a.username)
|
||||
self.assertEqual(password, a.password)
|
||||
self.assertEqual(trust_id, a.trust_id)
|
||||
self.assertEqual(tenant_id, a.tenant_id)
|
||||
|
||||
def test_loading_v3(self):
|
||||
section = uuid.uuid4().hex
|
||||
token = uuid.uuid4().hex
|
||||
trust_id = uuid.uuid4().hex
|
||||
project_id = uuid.uuid4().hex
|
||||
project_domain_name = uuid.uuid4().hex
|
||||
|
||||
self.conf_fixture.config(auth_section=section, group=self.GROUP)
|
||||
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||
|
||||
self.conf_fixture.register_opts(v3_auth.Token.get_options(),
|
||||
group=section)
|
||||
|
||||
self.conf_fixture.config(auth_plugin=self.V3TOKEN,
|
||||
token=token,
|
||||
trust_id=trust_id,
|
||||
project_id=project_id,
|
||||
project_domain_name=project_domain_name,
|
||||
group=section)
|
||||
|
||||
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||
|
||||
self.assertEqual(token, a.auth_methods[0].token)
|
||||
self.assertEqual(trust_id, a.trust_id)
|
||||
self.assertEqual(project_id, a.project_id)
|
||||
self.assertEqual(project_domain_name, a.project_domain_name)
|
||||
|
||||
def test_loading_invalid_plugin(self):
|
||||
self.conf_fixture.config(auth_plugin=uuid.uuid4().hex,
|
||||
group=self.GROUP)
|
||||
|
||||
self.assertRaises(exceptions.NoMatchingPlugin,
|
||||
conf.load_from_conf_options,
|
||||
self.conf_fixture.conf,
|
||||
self.GROUP)
|
||||
|
||||
def test_loading_with_no_data(self):
|
||||
self.assertRaises(exceptions.NoMatchingPlugin,
|
||||
conf.load_from_conf_options,
|
||||
self.conf_fixture.conf,
|
||||
self.GROUP)
|
||||
|
||||
@mock.patch('stevedore.DriverManager')
|
||||
def test_other_params(self, m):
|
||||
m.return_value = utils.MockManager(utils.MockPlugin)
|
||||
driver_name = uuid.uuid4().hex
|
||||
|
||||
self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
|
||||
group=self.GROUP)
|
||||
self.conf_fixture.config(auth_plugin=driver_name,
|
||||
group=self.GROUP,
|
||||
**self.TEST_VALS)
|
||||
|
||||
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||
self.assertTestVals(a)
|
||||
|
||||
m.assert_called_once_with(namespace=base.PLUGIN_NAMESPACE,
|
||||
name=driver_name,
|
||||
invoke_on_load=False)
|
||||
|
||||
@utils.mock_plugin
|
||||
def test_same_section(self, m):
|
||||
self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
|
||||
group=self.GROUP)
|
||||
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||
self.conf_fixture.config(auth_plugin=uuid.uuid4().hex,
|
||||
group=self.GROUP,
|
||||
**self.TEST_VALS)
|
||||
|
||||
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||
self.assertTestVals(a)
|
||||
|
||||
@utils.mock_plugin
|
||||
def test_diff_section(self, m):
|
||||
section = uuid.uuid4().hex
|
||||
|
||||
self.conf_fixture.config(auth_section=section, group=self.GROUP)
|
||||
conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
|
||||
|
||||
self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
|
||||
group=section)
|
||||
self.conf_fixture.config(group=section,
|
||||
auth_plugin=uuid.uuid4().hex,
|
||||
**self.TEST_VALS)
|
||||
|
||||
a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
|
||||
self.assertTestVals(a)
|
||||
|
||||
def test_plugins_are_all_opts(self):
|
||||
manager = stevedore.ExtensionManager(base.PLUGIN_NAMESPACE,
|
||||
invoke_on_load=False,
|
||||
propagate_map_exceptions=True)
|
||||
|
||||
def inner(driver):
|
||||
for p in driver.plugin.get_options():
|
||||
self.assertIsInstance(p, cfg.Opt)
|
||||
|
||||
manager.map(inner)
|
||||
|
||||
def test_get_common(self):
|
||||
opts = conf.get_common_conf_options()
|
||||
for opt in opts:
|
||||
self.assertIsInstance(opt, cfg.Opt)
|
||||
self.assertEqual(2, len(opts))
|
||||
|
||||
def test_get_named(self):
|
||||
loaded_opts = conf.get_plugin_options('v2password')
|
||||
plugin_opts = v2_auth.Password.get_options()
|
||||
|
||||
self.assertEqual(plugin_opts, loaded_opts)
|
|
@ -0,0 +1,83 @@
|
|||
# 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 mock
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
|
||||
from keystoneclient.auth import base
|
||||
from keystoneclient.tests import utils
|
||||
|
||||
|
||||
class MockPlugin(base.BaseAuthPlugin):
|
||||
|
||||
INT_DESC = 'test int'
|
||||
FLOAT_DESC = 'test float'
|
||||
BOOL_DESC = 'test bool'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._data = kwargs
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def get_token(self, *args, **kwargs):
|
||||
return 'aToken'
|
||||
|
||||
def get_endpoint(self, *args, **kwargs):
|
||||
return 'http://test'
|
||||
|
||||
@classmethod
|
||||
def get_options(cls):
|
||||
return [
|
||||
cfg.IntOpt('a-int', default='3', help=cls.INT_DESC),
|
||||
cfg.BoolOpt('a-bool', help=cls.BOOL_DESC),
|
||||
cfg.FloatOpt('a-float', help=cls.FLOAT_DESC),
|
||||
]
|
||||
|
||||
|
||||
class MockManager(object):
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
|
||||
def mock_plugin(f):
|
||||
@functools.wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
with mock.patch.object(base, 'get_plugin_class') as m:
|
||||
m.return_value = MockPlugin
|
||||
args = list(args) + [m]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class TestCase(utils.TestCase):
|
||||
|
||||
GROUP = 'auth'
|
||||
V2PASS = 'v2password'
|
||||
V3TOKEN = 'v3token'
|
||||
|
||||
a_int = 88
|
||||
a_float = 88.8
|
||||
a_bool = False
|
||||
|
||||
TEST_VALS = {'a_int': a_int,
|
||||
'a_float': a_float,
|
||||
'a_bool': a_bool}
|
||||
|
||||
def assertTestVals(self, plugin, vals=TEST_VALS):
|
||||
for k, v in six.iteritems(vals):
|
||||
self.assertEqual(v, plugin[k])
|
|
@ -7,3 +7,4 @@ pbr>=0.6,!=0.7,<1.0
|
|||
PrettyTable>=0.7,<0.8
|
||||
requests>=1.1
|
||||
six>=1.7.0
|
||||
stevedore>=0.14
|
||||
|
|
|
@ -31,6 +31,12 @@ setup-hooks =
|
|||
console_scripts =
|
||||
keystone = keystoneclient.shell:main
|
||||
|
||||
keystoneclient.auth.plugin =
|
||||
v2password = keystoneclient.auth.identity.v2:Password
|
||||
v2token = keystoneclient.auth.identity.v2:Token
|
||||
v3password = keystoneclient.auth.identity.v3:Password
|
||||
v3token = keystoneclient.auth.identity.v3:Token
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
|
|
|
@ -9,7 +9,6 @@ mox3>=0.7.0
|
|||
oauthlib>=0.6
|
||||
pycrypto>=2.6
|
||||
sphinx>=1.1.2,!=1.2.0,<1.3
|
||||
stevedore>=0.14
|
||||
testrepository>=0.0.18
|
||||
testresources>=0.2.4
|
||||
testtools>=0.9.34
|
||||
|
|
Loading…
Reference in New Issue