From 4b0ce57c736d406d7a757b2aefb399fe58e3b0ec Mon Sep 17 00:00:00 2001 From: Lakshmi N Sampath Date: Tue, 8 Sep 2015 19:08:36 +0530 Subject: [PATCH] Fix for Image members not generating notifications Image members CRUD doesn't generate notifications which is impacting searchlight service by not having latest changes to Image memberships. If you create an image and later change its members, the members are not updated via notifications. You have to run the index sync again to get the updated member list. See: https://bugs.launchpad.net/searchlight/+bug/1490697 Membership information is critical for horizon filtering. Typically, a person is allowed to view an image under the following conditions: 1) The image is owned by the project I am currently logged into. 2) The image is public 3) The image is owned by another project which has added me as a member and I have accepted membership to it. Without current membership information, 3) above is not possible. See: https://bugs.launchpad.net/searchlight/+bug/1491085 Change-Id: Ia56e42d3d8da36cfa419d5c3c7d69c9ccf8974fd Closes-Bug: #1441453 --- glance/api/authorization.py | 40 ++++++---- glance/api/policy.py | 20 +++-- glance/api/v2/image_members.py | 43 +++++------ glance/db/__init__.py | 5 -- glance/domain/proxy.py | 58 +++++++++++--- glance/gateway.py | 14 ++++ glance/location.py | 16 +++- glance/notifier.py | 57 ++++++++++++++ glance/quota/__init__.py | 14 +++- glance/tests/unit/test_domain_proxy.py | 39 ++-------- glance/tests/unit/test_notifier.py | 102 +++++++++++++++++++++++++ glance/tests/unit/test_policy.py | 4 +- glance/tests/unit/test_quota.py | 4 +- glance/tests/unit/test_store_image.py | 18 ++++- 14 files changed, 334 insertions(+), 100 deletions(-) diff --git a/glance/api/authorization.py b/glance/api/authorization.py index 7cea2c0da2..5af54118fa 100644 --- a/glance/api/authorization.py +++ b/glance/api/authorization.py @@ -112,13 +112,24 @@ class ImageRepoProxy(glance.domain.proxy.Repo): return [proxy_image(self.context, i) for i in images] -class ImageMemberRepoProxy(glance.domain.proxy.Repo): +class ImageMemberRepoProxy(glance.domain.proxy.MemberRepo): def __init__(self, member_repo, image, context): self.member_repo = member_repo self.image = image self.context = context - super(ImageMemberRepoProxy, self).__init__(member_repo) + proxy_kwargs = {'context': self.context} + super(ImageMemberRepoProxy, self).__init__( + image, + member_repo, + member_proxy_class=ImageMemberProxy, + member_proxy_kwargs=proxy_kwargs) + self._check_image_visibility() + + def _check_image_visibility(self): + if self.image.visibility == 'public': + message = _("Public images do not have members.") + raise exception.Forbidden(message) def get(self, member_id): if (self.context.is_admin or @@ -189,11 +200,16 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory): return super(ImageFactoryProxy, self).new_image(owner=owner, **kwargs) -class ImageMemberFactoryProxy(object): +class ImageMemberFactoryProxy(glance.domain.proxy.ImageMembershipFactory): def __init__(self, image_member_factory, context): self.image_member_factory = image_member_factory self.context = context + kwargs = {'context': self.context} + super(ImageMemberFactoryProxy, self).__init__( + image_member_factory, + proxy_class=ImageMemberProxy, + proxy_kwargs=kwargs) def new_image_member(self, image, member_id): owner = image.owner @@ -315,10 +331,6 @@ class ImmutableImageProxy(object): message = _("You are not permitted to delete this image.") raise exception.Forbidden(message) - def get_member_repo(self): - member_repo = self.base.get_member_repo() - return ImageMemberRepoProxy(member_repo, self, self.context) - def get_data(self, *args, **kwargs): return self.base.get_data(*args, **kwargs) @@ -401,13 +413,13 @@ class ImageProxy(glance.domain.proxy.Image): self.context = context super(ImageProxy, self).__init__(image) - def get_member_repo(self, **kwargs): - if self.image.visibility == 'public': - message = _("Public images do not have members.") - raise exception.Forbidden(message) - else: - member_repo = self.image.get_member_repo(**kwargs) - return ImageMemberRepoProxy(member_repo, self, self.context) + +class ImageMemberProxy(glance.domain.proxy.ImageMember): + + def __init__(self, image_member, context): + self.image_member = image_member + self.context = context + super(ImageMemberProxy, self).__init__(image_member) class TaskProxy(glance.domain.proxy.Task): diff --git a/glance/api/policy.py b/glance/api/policy.py index 72e2c0ce8f..96f99c72ac 100644 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -191,9 +191,14 @@ class ImageProxy(glance.domain.proxy.Image): self.policy.enforce(self.context, 'upload_image', self.target) return self.image.set_data(*args, **kwargs) - def get_member_repo(self, **kwargs): - member_repo = self.image.get_member_repo(**kwargs) - return ImageMemberRepoProxy(member_repo, self.context, self.policy) + +class ImageMemberProxy(glance.domain.proxy.ImageMember): + + def __init__(self, image_member, context, policy): + super(ImageMemberProxy, self).__init__(image_member) + self.image_member = image_member + self.context = context + self.policy = policy class ImageFactoryProxy(glance.domain.proxy.ImageFactory): @@ -218,15 +223,16 @@ class ImageMemberFactoryProxy(glance.domain.proxy.ImageMembershipFactory): def __init__(self, member_factory, context, policy): super(ImageMemberFactoryProxy, self).__init__( member_factory, - image_proxy_class=ImageProxy, - image_proxy_kwargs={'context': context, 'policy': policy}) + proxy_class=ImageMemberProxy, + proxy_kwargs={'context': context, 'policy': policy}) class ImageMemberRepoProxy(glance.domain.proxy.Repo): - def __init__(self, member_repo, context, policy): + def __init__(self, member_repo, image, context, policy): self.member_repo = member_repo - self.target = ImageTarget(self.member_repo.image) + self.image = image + self.target = ImageTarget(image) self.context = context self.policy = policy diff --git a/glance/api/v2/image_members.py b/glance/api/v2/image_members.py index 636c9f1f16..03f6429b1c 100644 --- a/glance/api/v2/image_members.py +++ b/glance/api/v2/image_members.py @@ -47,6 +47,18 @@ class ImageMembersController(object): self.gateway = glance.gateway.Gateway(self.db_api, self.store_api, self.notifier, self.policy) + def _get_member_repo(self, req, image): + try: + # For public images, a forbidden exception with message + # "Public images do not have members" is thrown. + return self.gateway.get_member_repo(image, req.context) + except exception.Forbidden as e: + msg = (_("Error fetching members of image %(image_id)s: " + "%(inner_msg)s") % {"image_id": image.image_id, + "inner_msg": e.msg}) + LOG.warning(msg) + raise webob.exc.HTTPForbidden(explanation=msg) + def _lookup_image(self, req, image_id): image_repo = self.gateway.get_repo(req.context) try: @@ -60,21 +72,8 @@ class ImageMembersController(object): LOG.warning(msg) raise webob.exc.HTTPForbidden(explanation=msg) - @staticmethod - def _get_member_repo(image): - try: - # For public images, a forbidden exception with message - # "Public images do not have members" is thrown. - return image.get_member_repo() - except exception.Forbidden as e: - msg = (_("Error fetching members of image %(image_id)s: " - "%(inner_msg)s") % {"image_id": image.image_id, - "inner_msg": e.msg}) - LOG.warning(msg) - raise webob.exc.HTTPForbidden(explanation=msg) - - def _lookup_member(self, image, member_id): - member_repo = self._get_member_repo(image) + def _lookup_member(self, req, image, member_id): + member_repo = self._get_member_repo(req, image) try: return member_repo.get(member_id) except (exception.NotFound): @@ -106,7 +105,7 @@ class ImageMembersController(object): """ image = self._lookup_image(req, image_id) - member_repo = self._get_member_repo(image) + member_repo = self._get_member_repo(req, image) image_member_factory = self.gateway.get_image_member_factory( req.context) try: @@ -148,8 +147,8 @@ class ImageMembersController(object): """ image = self._lookup_image(req, image_id) - member_repo = self._get_member_repo(image) - member = self._lookup_member(image, member_id) + member_repo = self._get_member_repo(req, image) + member = self._lookup_member(req, image, member_id) try: member.status = status member_repo.save(member) @@ -182,7 +181,7 @@ class ImageMembersController(object): ]} """ image = self._lookup_image(req, image_id) - member_repo = self._get_member_repo(image) + member_repo = self._get_member_repo(req, image) members = [] try: for member in member_repo.list(): @@ -209,7 +208,7 @@ class ImageMembersController(object): """ try: image = self._lookup_image(req, image_id) - return self._lookup_member(image, member_id) + return self._lookup_member(req, image, member_id) except webob.exc.HTTPForbidden as e: # Convert Forbidden to NotFound to prevent information # leakage. @@ -221,8 +220,8 @@ class ImageMembersController(object): Removes a membership from the image. """ image = self._lookup_image(req, image_id) - member_repo = self._get_member_repo(image) - member = self._lookup_member(image, member_id) + member_repo = self._get_member_repo(req, image) + member = self._lookup_member(req, image, member_id) try: member_repo.remove(member) return webob.Response(body='', status=204) diff --git a/glance/db/__init__.py b/glance/db/__init__.py index ed9a02af40..6c6e91d406 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -302,11 +302,6 @@ class ImageProxy(glance.domain.proxy.Image): self.image = image super(ImageProxy, self).__init__(image) - def get_member_repo(self): - member_repo = ImageMemberRepo(self.context, self.db_api, - self.image) - return member_repo - class ImageMemberRepo(object): diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index c9ce3e4bd7..9cc7bfef15 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -105,6 +105,37 @@ class Repo(object): return self.helper.proxy(result) +class MemberRepo(object): + def __init__(self, image, base, + member_proxy_class=None, member_proxy_kwargs=None): + self.image = image + self.base = base + self.member_proxy_helper = Helper(member_proxy_class, + member_proxy_kwargs) + + def get(self, member_id): + member = self.base.get(member_id) + return self.member_proxy_helper.proxy(member) + + def add(self, member): + self.base.add(self.member_proxy_helper.unproxy(member)) + + def list(self, *args, **kwargs): + members = self.base.list(*args, **kwargs) + return [self.member_proxy_helper.proxy(member) for member + in members] + + def remove(self, member): + base_item = self.member_proxy_helper.unproxy(member) + result = self.base.remove(base_item) + return self.member_proxy_helper.proxy(result) + + def save(self, member, from_state=None): + base_item = self.member_proxy_helper.unproxy(member) + result = self.base.save(base_item, from_state=from_state) + return self.member_proxy_helper.proxy(result) + + class ImageFactory(object): def __init__(self, base, proxy_class=None, proxy_kwargs=None): self.helper = Helper(proxy_class, proxy_kwargs) @@ -115,16 +146,14 @@ class ImageFactory(object): class ImageMembershipFactory(object): - def __init__(self, base, image_proxy_class=None, image_proxy_kwargs=None, - member_proxy_class=None, member_proxy_kwargs=None): + def __init__(self, base, proxy_class=None, proxy_kwargs=None): + self.helper = Helper(proxy_class, proxy_kwargs) self.base = base - self.image_helper = Helper(image_proxy_class, image_proxy_kwargs) - self.member_helper = Helper(member_proxy_class, member_proxy_kwargs) - def new_image_member(self, image, member_id): - base_image = self.image_helper.unproxy(image) - member = self.base.new_image_member(base_image, member_id) - return self.member_helper.proxy(member) + def new_image_member(self, image, member, **kwargs): + return self.helper.proxy(self.base.new_image_member(image, + member, + **kwargs)) class Image(object): @@ -168,8 +197,17 @@ class Image(object): def get_data(self, *args, **kwargs): return self.base.get_data(*args, **kwargs) - def get_member_repo(self): - return self.helper.proxy(self.base.get_member_repo()) + +class ImageMember(object): + def __init__(self, base): + self.base = base + + id = _proxy('base', 'id') + image_id = _proxy('base', 'image_id') + member_id = _proxy('base', 'member_id') + status = _proxy('base', 'status') + created_at = _proxy('base', 'created_at') + updated_at = _proxy('base', 'updated_at') class Task(object): diff --git a/glance/gateway.py b/glance/gateway.py index 6ecced5d52..c15d4d740f 100644 --- a/glance/gateway.py +++ b/glance/gateway.py @@ -89,6 +89,20 @@ class Gateway(object): return authorized_image_repo + def get_member_repo(self, image, context): + image_member_repo = glance.db.ImageMemberRepo( + context, self.db_api, image) + store_image_repo = glance.location.ImageMemberRepoProxy( + image_member_repo, image, context, self.store_api) + policy_member_repo = policy.ImageMemberRepoProxy( + store_image_repo, image, context, self.policy) + notifier_member_repo = glance.notifier.ImageMemberRepoProxy( + policy_member_repo, image, context, self.notifier) + authorized_member_repo = authorization.ImageMemberRepoProxy( + notifier_member_repo, image, context) + + return authorized_member_repo + def get_task_factory(self, context): task_factory = glance.domain.TaskFactory() policy_task_factory = policy.TaskFactoryProxy( diff --git a/glance/location.py b/glance/location.py index 0e29d18346..0b4fa45504 100644 --- a/glance/location.py +++ b/glance/location.py @@ -45,11 +45,16 @@ class ImageRepoProxy(glance.domain.proxy.Repo): item_proxy_class=ImageProxy, item_proxy_kwargs=proxy_kwargs) + self.db_api = glance.db.get_api() + def _set_acls(self, image): public = image.visibility == 'public' member_ids = [] if image.locations and not public: - member_repo = image.get_member_repo() + member_repo = _get_member_repo_for_store(image, + self.context, + self.db_api, + self.store_api) member_ids = [m.member_id for m in member_repo.list()] for location in image.locations: self.store_api.set_acls(location['url'], public=public, @@ -67,6 +72,15 @@ class ImageRepoProxy(glance.domain.proxy.Repo): return result +def _get_member_repo_for_store(image, context, db_api, store_api): + image_member_repo = glance.db.ImageMemberRepo( + context, db_api, image) + store_image_repo = glance.location.ImageMemberRepoProxy( + image_member_repo, image, context, store_api) + + return store_image_repo + + def _check_location_uri(context, store_api, store_utils, uri): """Check if an image location is valid. diff --git a/glance/notifier.py b/glance/notifier.py index 84a6b5440a..b81d529431 100644 --- a/glance/notifier.py +++ b/glance/notifier.py @@ -133,6 +133,21 @@ def format_image_notification(image): } +def format_image_member_notification(image_member): + """Given a glance.domain.ImageMember object, return a dictionary of relevant + notification information. + """ + return { + 'image_id': image_member.image_id, + 'member_id': image_member.member_id, + 'status': image_member.status, + 'created_at': timeutils.isotime(image_member.created_at), + 'updated_at': timeutils.isotime(image_member.updated_at), + 'deleted': False, + 'deleted_at': None, + } + + def format_task_notification(task): # NOTE(nikhil): input is not passed to the notifier payload as it may # contain sensitive info. @@ -436,6 +451,11 @@ class ImageProxy(NotificationProxy, domain_proxy.Image): self.send_notification('image.activate', self.repo) +class ImageMemberProxy(NotificationProxy, domain_proxy.ImageMember): + def get_super_class(self): + return domain_proxy.ImageMember + + class ImageFactoryProxy(NotificationFactoryProxy, domain_proxy.ImageFactory): def get_super_class(self): return domain_proxy.ImageFactory @@ -469,6 +489,43 @@ class ImageRepoProxy(NotificationRepoProxy, domain_proxy.Repo): }) +class ImageMemberRepoProxy(NotificationBase, domain_proxy.MemberRepo): + + def __init__(self, repo, image, context, notifier): + self.repo = repo + self.image = image + self.context = context + self.notifier = notifier + proxy_kwargs = {'context': self.context, 'notifier': self.notifier} + + proxy_class = self.get_proxy_class() + super_class = self.get_super_class() + super_class.__init__(self, image, repo, proxy_class, proxy_kwargs) + + def get_super_class(self): + return domain_proxy.MemberRepo + + def get_proxy_class(self): + return ImageMemberProxy + + def get_payload(self, obj): + return format_image_member_notification(obj) + + def save(self, member, from_state=None): + super(ImageMemberRepoProxy, self).save(member, from_state=from_state) + self.send_notification('image.member.update', member) + + def add(self, member): + super(ImageMemberRepoProxy, self).add(member) + self.send_notification('image.member.create', member) + + def remove(self, member): + super(ImageMemberRepoProxy, self).remove(member) + self.send_notification('image.member.delete', member, extra_payload={ + 'deleted': True, 'deleted_at': timeutils.isotime() + }) + + class TaskProxy(NotificationProxy, domain_proxy.Task): def get_super_class(self): return domain_proxy.Task diff --git a/glance/quota/__init__.py b/glance/quota/__init__.py index 1fc8079a7e..eb71f32bfb 100644 --- a/glance/quota/__init__.py +++ b/glance/quota/__init__.py @@ -169,8 +169,8 @@ class ImageMemberFactoryProxy(glance.domain.proxy.ImageMembershipFactory): 'store_utils': store_utils} super(ImageMemberFactoryProxy, self).__init__( member_factory, - image_proxy_class=ImageProxy, - image_proxy_kwargs=proxy_kwargs) + proxy_class=ImageMemberProxy, + proxy_kwargs=proxy_kwargs) def _enforce_image_member_quota(self, image): if CONF.image_member_quota < 0: @@ -368,3 +368,13 @@ class ImageProxy(glance.domain.proxy.Image): def added_new_properties(self): current_props = set(self.image.extra_properties.keys()) return bool(current_props.difference(self.orig_props)) + + +class ImageMemberProxy(glance.domain.proxy.ImageMember): + + def __init__(self, image_member, context, db_api, store_utils): + self.image_member = image_member + self.context = context + self.db_api = db_api + self.store_utils = store_utils + super(ImageMemberProxy, self).__init__(image_member) diff --git a/glance/tests/unit/test_domain_proxy.py b/glance/tests/unit/test_domain_proxy.py index 39b72becda..6979cab776 100644 --- a/glance/tests/unit/test_domain_proxy.py +++ b/glance/tests/unit/test_domain_proxy.py @@ -220,8 +220,7 @@ class TestImageMembershipFactory(test_utils.BaseTestCase): def test_proxy_wrapped_membership(self): proxy_factory = proxy.ImageMembershipFactory( - self.factory, member_proxy_class=FakeProxy, - member_proxy_kwargs={'a': 1}) + self.factory, proxy_class=FakeProxy, proxy_kwargs={'a': 1}) self.factory.result = 'tyrion' membership = proxy_factory.new_image_member('jaime', 'cersei') self.assertIsInstance(membership, FakeProxy) @@ -232,12 +231,12 @@ class TestImageMembershipFactory(test_utils.BaseTestCase): def test_proxy_wrapped_image(self): proxy_factory = proxy.ImageMembershipFactory( - self.factory, image_proxy_class=FakeProxy) + self.factory, proxy_class=FakeProxy) self.factory.result = 'tyrion' image = FakeProxy('jaime') membership = proxy_factory.new_image_member(image, 'cersei') - self.assertEqual('tyrion', membership) - self.assertEqual('jaime', self.factory.image) + self.assertIsInstance(membership, FakeProxy) + self.assertIsInstance(self.factory.image, FakeProxy) self.assertEqual('cersei', self.factory.member_id) def test_proxy_both_wrapped(self): @@ -246,9 +245,8 @@ class TestImageMembershipFactory(test_utils.BaseTestCase): proxy_factory = proxy.ImageMembershipFactory( self.factory, - member_proxy_class=FakeProxy, - member_proxy_kwargs={'b': 2}, - image_proxy_class=FakeProxy2) + proxy_class=FakeProxy, + proxy_kwargs={'b': 2}) self.factory.result = 'tyrion' image = FakeProxy2('jaime') @@ -256,7 +254,7 @@ class TestImageMembershipFactory(test_utils.BaseTestCase): self.assertIsInstance(membership, FakeProxy) self.assertEqual('tyrion', membership.base) self.assertEqual({'b': 2}, membership.kwargs) - self.assertEqual('jaime', self.factory.image) + self.assertIsInstance(self.factory.image, FakeProxy2) self.assertEqual('cersei', self.factory.member_id) @@ -264,29 +262,6 @@ class FakeImage(object): def __init__(self, result=None): self.result = result - def get_member_repo(self): - return self.result - - -class TestImage(test_utils.BaseTestCase): - def setUp(self): - super(TestImage, self).setUp() - self.image = FakeImage() - - def test_normal_member_repo(self): - proxy_image = proxy.Image(self.image) - self.image.result = 'mormont' - self.assertEqual('mormont', proxy_image.get_member_repo()) - - def test_proxied_member_repo(self): - proxy_image = proxy.Image(self.image, - member_repo_proxy_class=FakeProxy, - member_repo_proxy_kwargs={'a': 10}) - self.image.result = 'corn' - member_repo = proxy_image.get_member_repo() - self.assertIsInstance(member_repo, FakeProxy) - self.assertEqual('corn', member_repo.base) - class TestTaskFactory(test_utils.BaseTestCase): def setUp(self): diff --git a/glance/tests/unit/test_notifier.py b/glance/tests/unit/test_notifier.py index b0a32a4192..068a92ed34 100644 --- a/glance/tests/unit/test_notifier.py +++ b/glance/tests/unit/test_notifier.py @@ -66,6 +66,23 @@ class ImageRepoStub(object): return ['images_from_list'] +class ImageMemberRepoStub(object): + def remove(self, *args, **kwargs): + return 'image_member_from_remove' + + def save(self, *args, **kwargs): + return 'image_member_from_save' + + def add(self, *args, **kwargs): + return 'image_member_from_add' + + def get(self, *args, **kwargs): + return 'image_member_from_get' + + def list(self, *args, **kwargs): + return ['image_members_from_list'] + + class TaskStub(glance.domain.TaskStub): def run(self, executor): pass @@ -394,6 +411,91 @@ class TestImageNotifications(utils.BaseTestCase): self.assertIn('Failed', output_log['payload']) +class TestImageMemberNotifications(utils.BaseTestCase): + """Test Image Member Notifications work""" + + def setUp(self): + super(TestImageMemberNotifications, self).setUp() + self.context = glance.context.RequestContext(tenant=TENANT2, + user=USER1) + self.notifier = unit_test_utils.FakeNotifier() + + self.image = ImageStub( + image_id=UUID1, name='image-1', status='active', size=1024, + created_at=DATETIME, updated_at=DATETIME, owner=TENANT1, + visibility='public', container_format='ami', + tags=['one', 'two'], disk_format='ami', min_ram=128, + min_disk=10, checksum='ca425b88f047ce8ec45ee90e813ada91', + locations=['http://127.0.0.1']) + self.image_member = glance.domain.ImageMembership( + id=1, image_id=UUID1, member_id=TENANT1, created_at=DATETIME, + updated_at=DATETIME, status='accepted') + + self.image_member_repo_stub = ImageMemberRepoStub() + self.image_member_repo_proxy = glance.notifier.ImageMemberRepoProxy( + self.image_member_repo_stub, self.image, + self.context, self.notifier) + self.image_member_proxy = glance.notifier.ImageMemberProxy( + self.image_member, self.context, self.notifier) + + def _assert_image_member_with_notifier(self, output_log, deleted=False): + self.assertEqual(self.image_member.member_id, + output_log['payload']['member_id']) + self.assertEqual(self.image_member.image_id, + output_log['payload']['image_id']) + self.assertEqual(self.image_member.status, + output_log['payload']['status']) + self.assertEqual(timeutils.isotime(self.image_member.created_at), + output_log['payload']['created_at']) + self.assertEqual(timeutils.isotime(self.image_member.updated_at), + output_log['payload']['updated_at']) + + if deleted: + self.assertTrue(output_log['payload']['deleted']) + self.assertIsNotNone(output_log['payload']['deleted_at']) + else: + self.assertFalse(output_log['payload']['deleted']) + self.assertIsNone(output_log['payload']['deleted_at']) + + def test_image_member_add_notification(self): + self.image_member_repo_proxy.add(self.image_member_proxy) + output_logs = self.notifier.get_logs() + self.assertEqual(1, len(output_logs)) + output_log = output_logs[0] + self.assertEqual('INFO', output_log['notification_type']) + self.assertEqual('image.member.create', output_log['event_type']) + self._assert_image_member_with_notifier(output_log) + + def test_image_member_save_notification(self): + self.image_member_repo_proxy.save(self.image_member_proxy) + output_logs = self.notifier.get_logs() + self.assertEqual(1, len(output_logs)) + output_log = output_logs[0] + self.assertEqual('INFO', output_log['notification_type']) + self.assertEqual('image.member.update', output_log['event_type']) + self._assert_image_member_with_notifier(output_log) + + def test_image_member_delete_notification(self): + self.image_member_repo_proxy.remove(self.image_member_proxy) + output_logs = self.notifier.get_logs() + self.assertEqual(1, len(output_logs)) + output_log = output_logs[0] + self.assertEqual('INFO', output_log['notification_type']) + self.assertEqual('image.member.delete', output_log['event_type']) + self._assert_image_member_with_notifier(output_log, deleted=True) + + def test_image_member_get(self): + image_member = self.image_member_repo_proxy.get(TENANT1) + self.assertIsInstance(image_member, glance.notifier.ImageMemberProxy) + self.assertEqual('image_member_from_get', image_member.repo) + + def test_image_member_list(self): + image_members = self.image_member_repo_proxy.list() + self.assertIsInstance(image_members[0], + glance.notifier.ImageMemberProxy) + self.assertEqual('image_members_from_list', image_members[0].repo) + + class TestTaskNotifications(utils.BaseTestCase): """Test Task Notifications work""" diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py index b260255cb9..f987b017a3 100644 --- a/glance/tests/unit/test_policy.py +++ b/glance/tests/unit/test_policy.py @@ -349,8 +349,10 @@ class TestMemberPolicy(test_utils.BaseTestCase): def setUp(self): self.policy = mock.Mock() self.policy.enforce = mock.Mock() + self.image_stub = ImageStub(UUID1) + image = glance.api.policy.ImageProxy(self.image_stub, {}, self.policy) self.member_repo = glance.api.policy.ImageMemberRepoProxy( - MemberRepoStub(), {}, self.policy) + MemberRepoStub(), image, {}, self.policy) self.target = self.member_repo.target super(TestMemberPolicy, self).setUp() diff --git a/glance/tests/unit/test_quota.py b/glance/tests/unit/test_quota.py index 1fdebee3ff..f7f9d13fd2 100644 --- a/glance/tests/unit/test_quota.py +++ b/glance/tests/unit/test_quota.py @@ -605,7 +605,7 @@ class TestImageMemberQuotas(test_utils.BaseTestCase): self.image_member_factory.new_image_member(self.image, 'fake_id') nim = self.base_image_member_factory.new_image_member - nim .assert_called_once_with(self.image.base, 'fake_id') + nim.assert_called_once_with(self.image, 'fake_id') def test_new_image_member_unlimited_members(self): self.config(image_member_quota=-1) @@ -613,7 +613,7 @@ class TestImageMemberQuotas(test_utils.BaseTestCase): self.image_member_factory.new_image_member(self.image, 'fake_id') nim = self.base_image_member_factory.new_image_member - nim.assert_called_once_with(self.image.base, 'fake_id') + nim.assert_called_once_with(self.image, 'fake_id') def test_new_image_member_too_many_members(self): self.config(image_member_quota=0) diff --git a/glance/tests/unit/test_store_image.py b/glance/tests/unit/test_store_image.py index 9aaa097148..d4b407998c 100644 --- a/glance/tests/unit/test_store_image.py +++ b/glance/tests/unit/test_store_image.py @@ -741,6 +741,18 @@ class TestStoreImageRepo(utils.BaseTestCase): self.image_repo = glance.location.ImageRepoProxy(self.image_repo_stub, {}, self.store_api, store_utils) + patcher = mock.patch("glance.location._get_member_repo_for_store", + self.get_fake_member_repo) + patcher.start() + self.addCleanup(patcher.stop) + self.fake_member_repo = FakeMemberRepo(self.image, [TENANT1, TENANT2]) + self.image_member_repo = glance.location.ImageMemberRepoProxy( + self.fake_member_repo, + self.image, + {}, self.store_api) + + def get_fake_member_repo(self, image, context, db_api, store_api): + return FakeMemberRepo(self.image, [TENANT1, TENANT2]) def test_add_updates_acls(self): self.image_stub.locations = [{'url': 'foo', 'metadata': {}, @@ -794,10 +806,9 @@ class TestStoreImageRepo(utils.BaseTestCase): self.image_stub.locations = [{'url': 'glug', 'metadata': {}, 'status': 'active'}] self.image_stub.visibility = 'private' - member_repo = self.image.get_member_repo() membership = glance.domain.ImageMembership( UUID1, TENANT3, None, None, status='accepted') - member_repo.add(membership) + self.image_member_repo.add(membership) self.assertIn('glug', self.store_api.acls) acls = self.store_api.acls['glug'] self.assertFalse(acls['public']) @@ -808,10 +819,9 @@ class TestStoreImageRepo(utils.BaseTestCase): self.image_stub.locations = [{'url': 'glug', 'metadata': {}, 'status': 'active'}] self.image_stub.visibility = 'private' - member_repo = self.image.get_member_repo() membership = glance.domain.ImageMembership( UUID1, TENANT1, None, None, status='accepted') - member_repo.remove(membership) + self.image_member_repo.remove(membership) self.assertIn('glug', self.store_api.acls) acls = self.store_api.acls['glug'] self.assertFalse(acls['public'])