Browse Source

Add service_token for cinder-nova interaction

Service token will be passed along with user token to communicate
with services when dealing with long running tasks like
Create volume snapshot.

Partial-Implements: blueprint use-service-tokens

Change-Id: Id95beae0a46ab492756e0108039fefb28f4f0b69
changes/97/524497/5
nirajsingh 4 years ago
parent
commit
0787710c24
  1. 4
      cinder/compute/nova.py
  2. 36
      cinder/context.py
  3. 6
      cinder/exception.py
  4. 5
      cinder/opts.py
  5. 73
      cinder/service_auth.py
  6. 77
      cinder/tests/unit/test_service_auth.py
  7. 9
      releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml

4
cinder/compute/nova.py

@ -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,

36
cinder/context.py

@ -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

6
cinder/exception.py

@ -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.")

5
cinder/opts.py

@ -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

@ -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

@ -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)

9
releasenotes/notes/validate-expired-user-tokens-40b15322197653ae.yaml

@ -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…
Cancel
Save