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 import exception
|
||||
from cinder import service_auth
|
||||
|
||||
nova_opts = [
|
||||
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_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(
|
||||
CONF,
|
||||
NOVA_GROUP,
|
||||
|
@ -19,6 +19,8 @@
|
||||
|
||||
import copy
|
||||
|
||||
from keystoneauth1.access import service_catalog as ksa_service_catalog
|
||||
from keystoneauth1 import plugin
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
@ -46,6 +48,31 @@ CONF.register_opts(context_opts)
|
||||
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
|
||||
class RequestContext(context.RequestContext):
|
||||
"""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,
|
||||
read_deleted="no", project_name=None, remote_address=None,
|
||||
timestamp=None, quota_class=None, service_catalog=None,
|
||||
**kwargs):
|
||||
user_auth_plugin=None, **kwargs):
|
||||
"""Initialize RequestContext.
|
||||
|
||||
: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)
|
||||
elif self.is_admin and 'admin' not in self.roles:
|
||||
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):
|
||||
return self._read_deleted
|
||||
|
@ -1344,3 +1344,9 @@ class GPFSDriverUnsupportedOperation(VolumeBackendAPIException):
|
||||
|
||||
class InvalidName(Invalid):
|
||||
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 \
|
||||
cinder_scheduler_weights_volumenumber
|
||||
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.transfer import api as cinder_transfer_api
|
||||
from cinder.volume import api as cinder_volume_api
|
||||
@ -269,6 +270,10 @@ def list_opts():
|
||||
itertools.chain(
|
||||
cinder_keymgr_confkeymgr.key_mgr_opts,
|
||||
)),
|
||||
('service_user',
|
||||
itertools.chain(
|
||||
cinder_serviceauth.service_user_opts,
|
||||
)),
|
||||
('backend_defaults',
|
||||
itertools.chain(
|
||||
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