Merge "Add service_token for cinder-nova interaction"
This commit is contained in:
commit
7bbc95344d
@ -27,6 +27,7 @@ from requests import exceptions as request_exceptions
|
|||||||
|
|
||||||
from cinder.db import base
|
from cinder.db import base
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder import service_auth
|
||||||
|
|
||||||
nova_opts = [
|
nova_opts = [
|
||||||
cfg.StrOpt('region_name',
|
cfg.StrOpt('region_name',
|
||||||
@ -107,6 +108,9 @@ def novaclient(context, privileged_user=False, timeout=None, api_version=None):
|
|||||||
project_name=context.project_name,
|
project_name=context.project_name,
|
||||||
project_domain_id=context.project_domain_id)
|
project_domain_id=context.project_domain_id)
|
||||||
|
|
||||||
|
if CONF.auth_strategy == 'keystone':
|
||||||
|
n_auth = service_auth.get_auth_plugin(context, auth=n_auth)
|
||||||
|
|
||||||
keystone_session = ks_loading.load_session_from_conf_options(
|
keystone_session = ks_loading.load_session_from_conf_options(
|
||||||
CONF,
|
CONF,
|
||||||
NOVA_GROUP,
|
NOVA_GROUP,
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
from keystoneauth1.access import service_catalog as ksa_service_catalog
|
||||||
|
from keystoneauth1 import plugin
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_context import context
|
from oslo_context import context
|
||||||
from oslo_db.sqlalchemy import enginefacade
|
from oslo_db.sqlalchemy import enginefacade
|
||||||
@ -46,6 +48,31 @@ CONF.register_opts(context_opts)
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _ContextAuthPlugin(plugin.BaseAuthPlugin):
|
||||||
|
"""A keystoneauth auth plugin that uses the values from the Context.
|
||||||
|
|
||||||
|
Ideally we would use the plugin provided by auth_token middleware however
|
||||||
|
this plugin isn't serialized yet so we construct one from the serialized
|
||||||
|
auth data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_token, sc):
|
||||||
|
super(_ContextAuthPlugin, self).__init__()
|
||||||
|
|
||||||
|
self.auth_token = auth_token
|
||||||
|
self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc)
|
||||||
|
|
||||||
|
def get_token(self, *args, **kwargs):
|
||||||
|
return self.auth_token
|
||||||
|
|
||||||
|
def get_endpoint(self, session, service_type=None, interface=None,
|
||||||
|
region_name=None, service_name=None, **kwargs):
|
||||||
|
return self.service_catalog.url_for(service_type=service_type,
|
||||||
|
service_name=service_name,
|
||||||
|
interface=interface,
|
||||||
|
region_name=region_name)
|
||||||
|
|
||||||
|
|
||||||
@enginefacade.transaction_context_provider
|
@enginefacade.transaction_context_provider
|
||||||
class RequestContext(context.RequestContext):
|
class RequestContext(context.RequestContext):
|
||||||
"""Security context and request information.
|
"""Security context and request information.
|
||||||
@ -56,7 +83,7 @@ class RequestContext(context.RequestContext):
|
|||||||
def __init__(self, user_id=None, project_id=None, is_admin=None,
|
def __init__(self, user_id=None, project_id=None, is_admin=None,
|
||||||
read_deleted="no", project_name=None, remote_address=None,
|
read_deleted="no", project_name=None, remote_address=None,
|
||||||
timestamp=None, quota_class=None, service_catalog=None,
|
timestamp=None, quota_class=None, service_catalog=None,
|
||||||
**kwargs):
|
user_auth_plugin=None, **kwargs):
|
||||||
"""Initialize RequestContext.
|
"""Initialize RequestContext.
|
||||||
|
|
||||||
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
|
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
|
||||||
@ -100,6 +127,13 @@ class RequestContext(context.RequestContext):
|
|||||||
self.is_admin = policy.check_is_admin(self)
|
self.is_admin = policy.check_is_admin(self)
|
||||||
elif self.is_admin and 'admin' not in self.roles:
|
elif self.is_admin and 'admin' not in self.roles:
|
||||||
self.roles.append('admin')
|
self.roles.append('admin')
|
||||||
|
self.user_auth_plugin = user_auth_plugin
|
||||||
|
|
||||||
|
def get_auth_plugin(self):
|
||||||
|
if self.user_auth_plugin:
|
||||||
|
return self.user_auth_plugin
|
||||||
|
else:
|
||||||
|
return _ContextAuthPlugin(self.auth_token, self.service_catalog)
|
||||||
|
|
||||||
def _get_read_deleted(self):
|
def _get_read_deleted(self):
|
||||||
return self._read_deleted
|
return self._read_deleted
|
||||||
|
@ -1344,3 +1344,9 @@ class GPFSDriverUnsupportedOperation(VolumeBackendAPIException):
|
|||||||
|
|
||||||
class InvalidName(Invalid):
|
class InvalidName(Invalid):
|
||||||
message = _("An invalid 'name' value was provided. %(reason)s")
|
message = _("An invalid 'name' value was provided. %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUserTokenNoAuth(CinderException):
|
||||||
|
message = _("The [service_user] send_service_user_token option was "
|
||||||
|
"requested, but no service auth could be loaded. Please check "
|
||||||
|
"the [service_user] configuration section.")
|
||||||
|
@ -66,6 +66,7 @@ from cinder.scheduler.weights import capacity as \
|
|||||||
from cinder.scheduler.weights import volume_number as \
|
from cinder.scheduler.weights import volume_number as \
|
||||||
cinder_scheduler_weights_volumenumber
|
cinder_scheduler_weights_volumenumber
|
||||||
from cinder import service as cinder_service
|
from cinder import service as cinder_service
|
||||||
|
from cinder import service_auth as cinder_serviceauth
|
||||||
from cinder import ssh_utils as cinder_sshutils
|
from cinder import ssh_utils as cinder_sshutils
|
||||||
from cinder.transfer import api as cinder_transfer_api
|
from cinder.transfer import api as cinder_transfer_api
|
||||||
from cinder.volume import api as cinder_volume_api
|
from cinder.volume import api as cinder_volume_api
|
||||||
@ -269,6 +270,10 @@ def list_opts():
|
|||||||
itertools.chain(
|
itertools.chain(
|
||||||
cinder_keymgr_confkeymgr.key_mgr_opts,
|
cinder_keymgr_confkeymgr.key_mgr_opts,
|
||||||
)),
|
)),
|
||||||
|
('service_user',
|
||||||
|
itertools.chain(
|
||||||
|
cinder_serviceauth.service_user_opts,
|
||||||
|
)),
|
||||||
('backend_defaults',
|
('backend_defaults',
|
||||||
itertools.chain(
|
itertools.chain(
|
||||||
cinder_volume_driver.volume_opts,
|
cinder_volume_driver.volume_opts,
|
||||||
|
73
cinder/service_auth.py
Normal file
73
cinder/service_auth.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# 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 keystoneauth1 import loading as ks_loading
|
||||||
|
from keystoneauth1 import service_token
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
_SERVICE_AUTH = None
|
||||||
|
|
||||||
|
SERVICE_USER_GROUP = 'service_user'
|
||||||
|
|
||||||
|
service_user = cfg.OptGroup(
|
||||||
|
SERVICE_USER_GROUP,
|
||||||
|
title='Service token authentication type options',
|
||||||
|
help="""
|
||||||
|
Configuration options for service to service authentication using a service
|
||||||
|
token. These options allow to send a service token along with the
|
||||||
|
user's token when contacting external REST APIs.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
service_user_opts = [
|
||||||
|
cfg.BoolOpt('send_service_user_token',
|
||||||
|
default=False,
|
||||||
|
help="""
|
||||||
|
When True, if sending a user token to an REST API, also send a service token.
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF.register_group(service_user)
|
||||||
|
CONF.register_opts(service_user_opts, group=service_user)
|
||||||
|
|
||||||
|
ks_loading.register_session_conf_options(CONF, SERVICE_USER_GROUP)
|
||||||
|
ks_loading.register_auth_conf_options(CONF, SERVICE_USER_GROUP)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_globals():
|
||||||
|
"""For async unit test consistency."""
|
||||||
|
global _SERVICE_AUTH
|
||||||
|
_SERVICE_AUTH = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_plugin(context, auth=None):
|
||||||
|
if auth:
|
||||||
|
user_auth = auth
|
||||||
|
else:
|
||||||
|
user_auth = context.get_auth_plugin()
|
||||||
|
|
||||||
|
if CONF.service_user.send_service_user_token:
|
||||||
|
global _SERVICE_AUTH
|
||||||
|
if not _SERVICE_AUTH:
|
||||||
|
_SERVICE_AUTH = ks_loading.load_auth_from_conf_options(
|
||||||
|
CONF, group=SERVICE_USER_GROUP)
|
||||||
|
if _SERVICE_AUTH is None:
|
||||||
|
# This can happen if no auth_type is specified, which probably
|
||||||
|
# means there's no auth information in the [service_user] group
|
||||||
|
raise exception.ServiceUserTokenNoAuth()
|
||||||
|
return service_token.ServiceTokenAuthWrapper(
|
||||||
|
user_auth=user_auth, service_auth=_SERVICE_AUTH)
|
||||||
|
|
||||||
|
return user_auth
|
77
cinder/tests/unit/test_service_auth.py
Normal file
77
cinder/tests/unit/test_service_auth.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# 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 keystoneauth1.identity.generic import password
|
||||||
|
from keystoneauth1 import loading as ks_loading
|
||||||
|
from keystoneauth1 import service_token
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder import exception
|
||||||
|
from cinder import service_auth
|
||||||
|
from cinder import test
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceAuthTestCase(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ServiceAuthTestCase, self).setUp()
|
||||||
|
self.ctx = context.RequestContext('fake', 'fake')
|
||||||
|
service_auth.reset_globals()
|
||||||
|
|
||||||
|
@mock.patch.object(ks_loading, 'load_auth_from_conf_options')
|
||||||
|
def test_get_auth_plugin_no_wraps(self, mock_load):
|
||||||
|
context = mock.MagicMock()
|
||||||
|
context.get_auth_plugin.return_value = "fake"
|
||||||
|
|
||||||
|
result = service_auth.get_auth_plugin(context)
|
||||||
|
|
||||||
|
self.assertEqual("fake", result)
|
||||||
|
mock_load.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch.object(ks_loading, 'load_auth_from_conf_options')
|
||||||
|
def test_get_auth_plugin_wraps(self, mock_load):
|
||||||
|
self.flags(send_service_user_token=True, group='service_user')
|
||||||
|
result = service_auth.get_auth_plugin(self.ctx)
|
||||||
|
|
||||||
|
self.assertIsInstance(result, service_token.ServiceTokenAuthWrapper)
|
||||||
|
mock_load.assert_called_once_with(mock.ANY, group='service_user')
|
||||||
|
|
||||||
|
def test_service_auth_requested_but_no_auth_given(self):
|
||||||
|
self.flags(send_service_user_token=True, group='service_user')
|
||||||
|
|
||||||
|
self.assertRaises(exception.ServiceUserTokenNoAuth,
|
||||||
|
service_auth.get_auth_plugin, self.ctx)
|
||||||
|
|
||||||
|
@mock.patch.object(ks_loading, 'load_auth_from_conf_options')
|
||||||
|
def test_get_auth_plugin_with_auth(self, mock_load):
|
||||||
|
self.flags(send_service_user_token=True, group='service_user')
|
||||||
|
|
||||||
|
mock_load.return_value = password.Password
|
||||||
|
result = service_auth.get_auth_plugin(
|
||||||
|
self.ctx, auth=mock_load.return_value)
|
||||||
|
|
||||||
|
self.assertEqual(mock_load.return_value, result.user_auth)
|
||||||
|
self.assertIsInstance(result, service_token.ServiceTokenAuthWrapper)
|
||||||
|
mock_load.assert_called_once_with(mock.ANY, group='service_user')
|
||||||
|
|
||||||
|
def test_get_auth_plugin_with_auth_and_service_token_false(self):
|
||||||
|
self.flags(send_service_user_token=False, group='service_user')
|
||||||
|
|
||||||
|
n_auth = password.Password
|
||||||
|
result = service_auth.get_auth_plugin(self.ctx, auth=n_auth)
|
||||||
|
|
||||||
|
self.assertEqual(n_auth, result)
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added support for Keystone middleware feature to pass service token along with the
|
||||||
|
user token for Cinder to Nova interaction. This will help get rid of user token
|
||||||
|
expiration issues during long running tasks e.g. creating volume snapshot.
|
||||||
|
To use this functionality a service user needs to be created first. Add the service
|
||||||
|
user configurations in ``cinder.conf`` under ``service_user`` group and set
|
||||||
|
``send_service_user_token`` flag to ``True``.
|
Loading…
Reference in New Issue
Block a user