diff --git a/openstack_auth/tests/tests.py b/openstack_auth/tests/tests.py index 4e596a3..b39936a 100644 --- a/openstack_auth/tests/tests.py +++ b/openstack_auth/tests/tests.py @@ -489,6 +489,51 @@ class OpenStackAuthTestsV2(test.TestCase): debug=False) self.assertEqual(tenant_list, expected_tenants) + def test_tenant_list_caching(self): + tenants = [self.data.tenant_two, self.data.tenant_one] + expected_tenants = [self.data.tenant_one, self.data.tenant_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + + self.mox.StubOutWithMock(self.ks_client_module, "Client") + self.mox.StubOutWithMock(self.keystone_client_unscoped.tenants, "list") + + self.ks_client_module.Client(user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False)\ + .AndReturn(self.keystone_client_unscoped) + self.keystone_client_unscoped.tenants.list().AndReturn(tenants) + + self.mox.ReplayAll() + + tenant_list = utils.get_project_list( + user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False) + self.assertEqual(tenant_list, expected_tenants) + + # Test to validate that requesting the project list again results + # to using the cache and will not make a Keystone call. + self.assertEqual(utils._PROJECT_CACHE.get(unscoped.auth_token), + expected_tenants) + tenant_list = utils.get_project_list( + user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False) + self.assertEqual(tenant_list, expected_tenants) + + utils.remove_project_cache(unscoped.auth_token) + self.assertIsNone(utils._PROJECT_CACHE.get(unscoped.auth_token)) + def EndpointMetaFactory(endpoint_type): def endpoint_wrapper(func): @@ -979,6 +1024,53 @@ class OpenStackAuthTestsV3(test.TestCase): debug=False) self.assertEqual(project_list, expected_projects) + def test_tenant_list_caching(self): + projects = [self.data.project_two, self.data.project_one] + expected_projects = [self.data.project_one, self.data.project_two] + user = self.data.user + unscoped = self.data.unscoped_access_info + + self.mox.StubOutWithMock(self.ks_client_module, "Client") + self.mox.StubOutWithMock(self.keystone_client_unscoped.projects, + "list") + + self.ks_client_module.Client(user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False)\ + .AndReturn(self.keystone_client_unscoped) + self.keystone_client_unscoped.projects.list(user=user.id) \ + .AndReturn(projects) + + self.mox.ReplayAll() + + project_list = utils.get_project_list( + user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False) + self.assertEqual(project_list, expected_projects) + + # Test to validate that requesting the project list again results + # to using the cache and will not make a Keystone call. + self.assertEqual(utils._PROJECT_CACHE.get(unscoped.auth_token), + expected_projects) + project_list = utils.get_project_list( + user_id=user.id, + auth_url=settings.OPENSTACK_KEYSTONE_URL, + token=unscoped.auth_token, + insecure=False, + cacert=None, + debug=False) + self.assertEqual(project_list, expected_projects) + + utils.remove_project_cache(unscoped.auth_token) + self.assertIsNone(utils._PROJECT_CACHE.get(unscoped.auth_token)) + class OpenStackAuthTestsV3WithPublicURL(OpenStackAuthTestsV3): """Test V3 with settings.OPENSTACK_ENDPOINT_TYPE = 'publicURL'.""" diff --git a/openstack_auth/utils.py b/openstack_auth/utils.py index 482b7a4..f6da16d 100644 --- a/openstack_auth/utils.py +++ b/openstack_auth/utils.py @@ -11,18 +11,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools + from six.moves.urllib import parse as urlparse from django.conf import settings from django.contrib import auth from django.contrib.auth import middleware from django.contrib.auth import models +from django.utils import decorators from django.utils import timezone from keystoneclient.v2_0 import client as client_v2 from keystoneclient.v3 import client as client_v3 +_PROJECT_CACHE = {} + + """ We need the request object to get the user, so we'll slightly modify the existing django.contrib.auth.get_user method. To do so we update the @@ -91,6 +97,37 @@ def is_safe_url(url, host=None): return not netloc or netloc == host +def memoize_by_keyword_arg(cache, kw_keys): + """Memoize a function using the list of keyword argument name as its key. + + Wrap a function so that results for any keyword argument tuple are stored + in 'cache'. Note that the keyword args to the function must be usable as + dictionary keys. + + :param cache: Dictionary object to store the results. + :param kw_keys: List of keyword arguments names. The values are used + for generating the key in the cache. + """ + def _decorator(func): + @functools.wraps(func, assigned=decorators.available_attrs(func)) + def wrapper(*args, **kwargs): + mem_args = [kwargs[key] for key in kw_keys if key in kwargs] + mem_args = '__'.join(str(mem_arg) for mem_arg in mem_args) + if not mem_args: + return func(*args, **kwargs) + if mem_args in cache: + return cache[mem_args] + result = func(*args, **kwargs) + cache[mem_args] = result + return result + return wrapper + return _decorator + + +def remove_project_cache(token): + _PROJECT_CACHE.pop(token, None) + + # Helper for figuring out keystone version # Implementation will change when API version discovery is available def get_keystone_version(): @@ -104,6 +141,7 @@ def get_keystone_client(): return client_v3 +@memoize_by_keyword_arg(_PROJECT_CACHE, ('token', )) def get_project_list(*args, **kwargs): if get_keystone_version() < 3: auth_url = kwargs.get('auth_url', '').replace('v3', 'v2.0') diff --git a/openstack_auth/views.py b/openstack_auth/views.py index 380223c..4ce09b7 100644 --- a/openstack_auth/views.py +++ b/openstack_auth/views.py @@ -119,6 +119,7 @@ def delete_token(endpoint, token_id): insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) ca_cert = getattr(settings, "OPENSTACK_SSL_CACERT", None) + utils.remove_project_cache(token_id) try: if utils.get_keystone_version() < 3: client = keystone_client_v2.Client(