Centralize session creation and authorization from OS clients

Recently novaclient moved to keystoneauth. When a token is passed to it,
an instance of Token auth plugin is used to handle it. The problem is
that it tries to reauthenticate using the token. It is not possible
with trust-scoped token.

Here we centralize the session establishment for service client creation
in most actions that enables the reuse of the existing token.

Change-Id: Ibe9ee28a027e7a782adb8d8120d745259c4608da
Co-Authored-By: Andras Kovi <akovi@nokia.com>
Closes-Bug: 1690787
This commit is contained in:
Andras Kovi 2017-06-14 16:31:30 +02:00
parent 1d71987243
commit f2c8e0c2a1
7 changed files with 131 additions and 240 deletions

View File

@ -42,9 +42,8 @@ def _try_import(module_name):
try:
return importutils.try_import(module_name)
except Exception as e:
msg = 'Unable to load module "%s". %s' % (module_name, e.message)
msg = 'Unable to load module "%s". %s' % (module_name, str(e))
LOG.error(msg)
print('ERROR [%s] %s' % (__name__, msg))
return None
@ -72,43 +71,24 @@ zaqarclient = _try_import('zaqarclient.queues.v2.client')
class NovaAction(base.OpenStackAction):
_service_name = 'nova'
_service_type = 'compute'
def _create_client(self, context):
@classmethod
def _get_client_class(cls):
return novaclient.Client
def _create_client(self, context):
LOG.debug("Nova action security context: %s" % context)
keystone_endpoint = keystone_utils.get_keystone_endpoint_v2()
nova_endpoint = self.get_service_endpoint()
client = novaclient.Client(
2,
username=None,
api_key=None,
endpoint_type=CONF.openstack_actions.os_actions_endpoint_type,
service_type='compute',
auth_token=context.auth_token,
tenant_id=context.project_id,
region_name=nova_endpoint.region,
auth_url=keystone_endpoint.url,
insecure=context.insecure
)
client.client.management_url = keystone_utils.format_url(
nova_endpoint.url,
{'tenant_id': context.project_id}
)
return client
return novaclient.Client(2, **self.get_session_and_auth(context))
@classmethod
def _get_fake_client(cls):
return novaclient.Client(2)
return cls._get_client_class()(2)
class GlanceAction(base.OpenStackAction):
_service_name = 'glance'
_service_type = 'image'
@classmethod
def _get_client_class(cls):
@ -123,8 +103,7 @@ class GlanceAction(base.OpenStackAction):
return self._get_client_class()(
glance_endpoint.url,
region_name=glance_endpoint.region,
token=context.auth_token,
insecure=context.insecure
**self.get_session_and_auth(context)
)
@classmethod
@ -134,6 +113,8 @@ class GlanceAction(base.OpenStackAction):
class KeystoneAction(base.OpenStackAction):
_service_type = 'identity'
@classmethod
def _get_client_class(cls):
return keystoneclient.Client
@ -142,26 +123,15 @@ class KeystoneAction(base.OpenStackAction):
LOG.debug("Keystone action security context: %s" % context)
# TODO(akovi) cacert is deprecated in favor of session
# TODO(akovi) this piece of code should be refactored
# TODO(akovi) to follow the new guide lines
kwargs = {
'token': context.auth_token,
'auth_url': context.auth_uri,
'project_id': context.project_id,
'cacert': context.auth_cacert,
'insecure': context.insecure
}
kwargs = self.get_session_and_auth(context)
# In case of trust-scoped token explicitly pass endpoint parameter.
if (context.is_trust_scoped
or keystone_utils.is_token_trust_scoped(context.auth_token)):
kwargs['endpoint'] = context.auth_uri
# NOTE(akovi): the endpoint in the token messes up
# keystone. The auth parameter should not be provided for
# these operations.
kwargs.pop('auth')
client = self._get_client_class()(**kwargs)
client.management_url = context.auth_uri
return client
@classmethod
@ -179,7 +149,7 @@ class KeystoneAction(base.OpenStackAction):
class CeilometerAction(base.OpenStackAction):
_service_name = 'ceilometer'
_service_type = 'metering'
@classmethod
def _get_client_class(cls):
@ -210,7 +180,7 @@ class CeilometerAction(base.OpenStackAction):
class HeatAction(base.OpenStackAction):
_service_name = 'heat'
_service_type = 'orchestration'
@classmethod
def _get_client_class(cls):
@ -233,9 +203,7 @@ class HeatAction(base.OpenStackAction):
return self._get_client_class()(
endpoint_url,
region_name=heat_endpoint.region,
token=context.auth_token,
username=context.user_name,
insecure=context.insecure
**self.get_session_and_auth(context)
)
@classmethod
@ -244,7 +212,7 @@ class HeatAction(base.OpenStackAction):
class NeutronAction(base.OpenStackAction):
_service_name = 'neutron'
_service_type = 'network'
@classmethod
def _get_client_class(cls):
@ -306,6 +274,7 @@ class CinderAction(base.OpenStackAction):
class MistralAction(base.OpenStackAction):
_service_type = 'workflowv2'
@classmethod
def _get_client_class(cls):
@ -315,17 +284,10 @@ class MistralAction(base.OpenStackAction):
LOG.debug("Mistral action security context: %s" % context)
mistral_endpoint = keystone_utils.get_endpoint_for_project(
'mistral'
)
mistral_url = mistral_endpoint.url
session_and_auth = self.get_session_and_auth(context)
return self._get_client_class()(
mistral_url=mistral_url,
auth_token=context.auth_token,
project_id=context.project_id,
user_id=context.user_id,
insecure=context.insecure
mistral_url=session_and_auth['auth'].endpoint,
**session_and_auth
)
@classmethod
@ -501,9 +463,9 @@ class ZaqarAction(base.OpenStackAction):
def wrap(*args, **kwargs):
return method(client, *args, **kwargs)
args = inspect_utils.get_arg_list_as_str(method)
arguments = inspect_utils.get_arg_list_as_str(method)
# Remove client
wrap.__arguments__ = args.split(', ', 1)[1]
wrap.__arguments__ = arguments.split(', ', 1)[1]
return wrap
@ -511,6 +473,9 @@ class ZaqarAction(base.OpenStackAction):
def queue_messages(client, queue_name, **params):
"""Gets a list of messages from the queue.
:param client: the Zaqar client
:type client: zaqarclient.queues.client
:param queue_name: Name of the target queue.
:type queue_name: `six.string_type`
@ -528,6 +493,9 @@ class ZaqarAction(base.OpenStackAction):
def queue_post(client, queue_name, messages):
"""Posts one or more messages to a queue.
:param client: the Zaqar client
:type client: zaqarclient.queues.client
:param queue_name: Name of the target queue.
:type queue_name: `six.string_type`
@ -545,6 +513,9 @@ class ZaqarAction(base.OpenStackAction):
def queue_pop(client, queue_name, count=1):
"""Pop `count` messages from the queue.
:param client: the Zaqar client
:type client: zaqarclient.queues.client
:param queue_name: Name of the target queue.
:type queue_name: `six.string_type`
@ -604,10 +575,10 @@ class BarbicanAction(base.OpenStackAction):
def wrap(*args, **kwargs):
return method(client, *args, **kwargs)
args = inspect_utils.get_arg_list_as_str(method)
arguments = inspect_utils.get_arg_list_as_str(method)
# Remove client.
wrap.__arguments__ = args.split(', ', 1)[1]
wrap.__arguments__ = arguments.split(', ', 1)[1]
return wrap
@ -621,6 +592,9 @@ class BarbicanAction(base.OpenStackAction):
mode=None, expiration=None):
"""Create and Store a secret in Barbican.
:param client: the Zaqar client
:type client: zaqarclient.queues.client
:param name: A friendly name for the Secret
:type name: string
@ -805,13 +779,13 @@ class SenlinAction(base.OpenStackAction):
insecure=context.insecure
)
@classmethod
def _get_fake_client(cls):
return cls._get_client_class()("http://127.0.0.1:8778")
@classmethod
def _get_fake_client(cls):
return cls._get_client_class()("http://127.0.0.1:8778")
class AodhAction(base.OpenStackAction):
_service_name = 'aodh'
_service_type = 'alarming'
@classmethod
def _get_client_class(cls):
@ -842,7 +816,7 @@ class AodhAction(base.OpenStackAction):
class GnocchiAction(base.OpenStackAction):
_service_name = 'gnocchi'
_service_type = 'metric'
@classmethod
def _get_client_class(cls):

View File

@ -16,16 +16,12 @@ import abc
import inspect
import traceback
from cachetools import LRUCache
from oslo_log import log
from mistral import exceptions as exc
from mistral.utils.openstack import keystone as keystone_utils
from mistral_lib import actions
from threading import Lock
LOG = log.getLogger(__name__)
@ -37,23 +33,22 @@ class OpenStackAction(actions.Action):
"""
_kwargs_for_run = {}
client_method_name = None
_clients = LRUCache(100)
_lock = Lock()
_service_name = None
_service_type = None
_client_class = None
def __init__(self, **kwargs):
self._kwargs_for_run = kwargs
self.action_region = self._kwargs_for_run.pop('action_region', None)
@abc.abstractmethod
def _create_client(self):
"""Creates client required for action operation"""
pass
def _create_client(self, context):
"""Creates client required for action operation."""
return None
@classmethod
def _get_client_class(cls):
return None
return cls._client_class
@classmethod
def _get_client_method(cls, client):
@ -86,40 +81,20 @@ class OpenStackAction(actions.Action):
(e.g. Nova, Glance, Heat, Keystone etc)
"""
# TODO(d0ugal): Caching has caused some security problems and
# regressions in Mistral. It is disabled for now and
# will be revisited in Ocata. See:
# https://bugs.launchpad.net/mistral/+bug/1627689
return self._create_client(context)
client_class = self.__class__.__name__
# Colon character is reserved (rfc3986) which avoids key collisions.
key = client_class + ':' + context.project_id
def get_session_and_auth(self, context):
"""Get keystone session and auth parameters.
def create_cached_client():
new_client = self._create_client(context)
new_client._mistral_ctx_expires_at = context.expires_at
:param context: the action context
:return: dict that can be used to initialize service clients
"""
with self._lock:
self._clients[key] = new_client
return new_client
with self._lock:
client = self._clients.get(key)
if client is None:
return create_cached_client()
if keystone_utils.will_expire_soon(client._mistral_ctx_expires_at):
LOG.debug("cache expiring soon, will refresh client")
return create_cached_client()
LOG.debug("cache not expiring soon, will return cached client")
return client
return keystone_utils.get_session_and_auth(
service_name=self._service_name,
service_type=self._service_type,
region_name=self.action_region,
context=context)
def get_service_endpoint(self):
"""Get OpenStack service endpoint.
@ -150,7 +125,10 @@ class OpenStackAction(actions.Action):
# where the issue comes from.
LOG.warning(traceback.format_exc())
e_str = '%s: %s' % (type(e), e.message)
if hasattr(e, 'message'):
e_str = '%s: %s' % (type(e), e.message)
else:
e_str = str(e)
raise exc.ActionException(
"%s.%s failed: %s" %

View File

@ -35,11 +35,11 @@ _periodic_tasks = {}
class MistralPeriodicTasks(periodic_task.PeriodicTasks):
@periodic_task.periodic_task(spacing=1, run_immediately=True)
def process_cron_triggers_v2(self, ctx):
for t in triggers.get_next_cron_triggers():
LOG.debug("Processing cron trigger: %s" % t)
for trigger in triggers.get_next_cron_triggers():
LOG.debug("Processing cron trigger: %s" % trigger)
# Setup admin context before schedule triggers.
ctx = security.create_context(t.trust_id, t.project_id)
ctx = security.create_context(trigger.trust_id, trigger.project_id)
auth_ctx.set_ctx(ctx)
@ -48,25 +48,27 @@ class MistralPeriodicTasks(periodic_task.PeriodicTasks):
try:
# Try to advance the cron trigger next_execution_time and
# remaining_executions if relevant.
modified = advance_cron_trigger(t)
modified = advance_cron_trigger(trigger)
# If cron trigger was not already modified by another engine.
if modified:
LOG.debug(
"Starting workflow '%s' by cron trigger '%s'",
t.workflow.name, t.name
trigger.workflow.name, trigger.name
)
rpc.get_engine_client().start_workflow(
t.workflow.name,
t.workflow_input,
trigger.workflow.name,
trigger.workflow_input,
description="Workflow execution created "
"by cron trigger.",
**t.workflow_params
**trigger.workflow_params
)
except Exception:
# Log and continue to next cron trigger.
LOG.exception("Failed to process cron trigger %s" % str(t))
LOG.exception(
"Failed to process cron trigger %s" % str(trigger)
)
finally:
auth_ctx.set_ctx(None)

View File

@ -95,6 +95,7 @@ def delete_trust(trust_id):
def add_trust_id(secure_object_values):
if cfg.CONF.pecan.auth_enable:
trust = create_trust()
secure_object_values.update({
'trust_id': create_trust().id
'trust_id': trust.id
})

View File

@ -108,7 +108,7 @@ def create_cron_trigger(name, workflow_name, workflow_input,
wf_spec.__class__.__name__
)
values = {
trigger_parameters = {
'name': name,
'pattern': pattern,
'first_execution_time': first_time,
@ -121,13 +121,13 @@ def create_cron_trigger(name, workflow_name, workflow_input,
'scope': 'private'
}
security.add_trust_id(values)
security.add_trust_id(trigger_parameters)
try:
trig = db_api.create_cron_trigger(values)
trig = db_api.create_cron_trigger(trigger_parameters)
except Exception:
# Delete trust before raising exception.
security.delete_trust(values.get('trust_id'))
security.delete_trust(trigger_parameters.get('trust_id'))
raise
return trig

View File

@ -15,7 +15,6 @@
import mock
from mistral.actions.openstack import actions
from mistral import context as ctx
from oslotest import base
@ -38,117 +37,6 @@ class OpenStackActionTest(base.BaseTestCase):
self.assertTrue(mocked().servers.get.called)
mocked().servers.get.assert_called_once_with(server="1234-abcd")
@mock.patch('mistral.actions.openstack.actions.keystone_utils.get_'
'keystone_endpoint_v2')
@mock.patch('mistral.actions.openstack.actions.keystone_utils.get_'
'endpoint_for_project')
@mock.patch('mistral.actions.openstack.actions.novaclient')
def test_nova_action_config_endpoint(self, mock_novaclient,
mock_nova_endpoint,
mock_ks_endpoint_v2):
test_ctx = ctx.MistralContext(
user=None,
tenant='1234',
project_name='admin',
auth_token=None,
is_admin=False,
# set year to 3016 in order for token to always be valid
expires_at='3016-07-13T18:34:22.000000Z',
insecure=False
)
# attributes mirror keystone Endpoint object exactly
# (with endpoint type public)
keystone_attrs = {
'url': 'http://192.0.2.1:5000/v2.0',
'enabled': True,
'id': 'b1ddf133fa6e491c8ee13701be97db2d',
'interface': 'public',
'links': {
u'self': u'http://192.0.2.1:5000/v3/endpoints/'
'b1ddf133fa6e491c8ee13701be97db2d'
},
'region': 'regionOne',
'region_id': 'regionOne',
'service_id': '8f4afc75cd584d5cb381f68a9db80147',
}
keystone_endpoint = FakeEndpoint(**keystone_attrs)
nova_attrs = {
'url': 'http://192.0.2.1:8774/v2/%(tenant_id)s',
'enabled': True,
'id': '5bb51b33c9984513b52b6a3e85154305',
'interface': 'public',
'links': {
u'self': u'http://192.0.2.1:5000/v3/endpoints/'
'5bb51b33c9984513b52b6a3e85154305'
},
'region': 'regionOne',
'region_id': 'regionOne',
'service_id': '1af46173f37848edb65bd4962ed2d09d',
}
nova_endpoint = FakeEndpoint(**nova_attrs)
mock_ks_endpoint_v2.return_value(keystone_endpoint)
mock_nova_endpoint.return_value(nova_endpoint)
method_name = "servers.get"
action_class = actions.NovaAction
action_class.client_method_name = method_name
params = {'server': '1234-abcd'}
action = action_class(**params)
action.run(test_ctx)
mock_novaclient.Client.assert_called_once_with(
2,
username=None,
api_key=None,
endpoint_type='public',
service_type='compute',
auth_token=test_ctx.auth_token,
tenant_id=test_ctx.project_id,
region_name=mock_nova_endpoint().region,
auth_url=mock_ks_endpoint_v2().url,
insecure=test_ctx.insecure
)
self.assertTrue(mock_novaclient.Client().servers.get.called)
mock_novaclient.Client().servers.get.assert_called_once_with(
server="1234-abcd")
# Repeat test in order to validate cache.
mock_novaclient.reset_mock()
action.run(test_ctx)
# TODO(d0ugal): Uncomment the following line when caching is fixed.
# mock_novaclient.Client.assert_not_called()
mock_novaclient.Client().servers.get.assert_called_with(
server="1234-abcd")
# Repeat again with different context for cache testing.
test_ctx.project_name = 'service'
test_ctx.project_id = '1235'
mock_novaclient.reset_mock()
action.run(test_ctx)
mock_novaclient.Client.assert_called_once_with(
2,
username=None,
api_key=None,
endpoint_type='public',
service_type='compute',
auth_token=test_ctx.auth_token,
tenant_id=test_ctx.project_id,
region_name=mock_nova_endpoint().region,
auth_url=mock_ks_endpoint_v2().url,
insecure=test_ctx.insecure
)
self.assertTrue(mock_novaclient.Client().servers.get.called)
mock_novaclient.Client().servers.get.assert_called_once_with(
server="1234-abcd")
@mock.patch.object(actions.GlanceAction, '_get_client')
def test_glance_action(self, mocked):
mock_ctx = mock.Mock()

View File

@ -15,6 +15,7 @@
import keystoneauth1.identity.generic as auth_plugins
from keystoneauth1 import session as ks_session
from keystoneauth1.token_endpoint import Token
from keystoneclient import service_catalog as ks_service_catalog
from keystoneclient.v3 import client as ks_client
from keystoneclient.v3 import endpoints as ks_endpoints
@ -44,6 +45,52 @@ def client():
return cl
def _determine_verify(ctx):
if ctx.insecure:
return False
elif ctx.auth_cacert:
return ctx.auth_cacert
else:
return True
def get_session_and_auth(context, **kwargs):
"""Get session and auth parameters
:param context: action context
:return: dict to be used as kwargs for client serviceinitialization
"""
if not context:
raise AssertionError('context is mandatory')
project_endpoint = get_endpoint_for_project(**kwargs)
endpoint = format_url(
project_endpoint.url,
{
'tenant_id': context.project_id,
'project_id': context.project_id
}
)
auth = Token(endpoint=endpoint, token=context.auth_token)
auth_uri = context.auth_uri or CONF.keystone_authtoken.auth_uri
ks_auth = Token(
endpoint=auth_uri,
token=context.auth_token
)
session = ks_session.Session(
auth=ks_auth,
verify=_determine_verify(context)
)
return {
"session": session,
"auth": auth
}
def _admin_client(trust_id=None, project_name=None):
auth_url = CONF.keystone_authtoken.auth_uri
@ -144,10 +191,11 @@ def obtain_service_catalog(ctx):
)
trust_client = client_for_trusts(ctx.trust_id)
response = trust_client.tokens.get_token_data(
token_data = trust_client.tokens.get_token_data(
token,
include_catalog=True
)['token']
)
response = token_data['token']
else:
response = ctx.service_catalog