# All Rights Reserved. # # 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. import mock from neutron_lib import context from neutron.agent import resource_cache from neutron.api.rpc.callbacks import events as events_rpc from neutron.callbacks import events from neutron.callbacks import registry from neutron.tests import base class OVOLikeThing(object): def __init__(self, id, revision_number=10, **kwargs): self.id = id self.fields = ['id', 'revision_number'] self.revision_number = revision_number for k, v in kwargs.items(): self.fields.append(k) setattr(self, k, v) def to_dict(self): return {f: getattr(self, f) for f in self.fields} def get(self, k): return getattr(self, k, None) class RemoteResourceCacheTestCase(base.BaseTestCase): def setUp(self): super(RemoteResourceCacheTestCase, self).setUp() rtypes = ['duck', 'goose'] self.goose = OVOLikeThing(1) self.duck = OVOLikeThing(2) self.ctx = context.get_admin_context() self.rcache = resource_cache.RemoteResourceCache(rtypes) self._pullmock = mock.patch.object(self.rcache, '_puller').start() def test_get_resource_by_id(self): self.rcache.record_resource_update(self.ctx, 'goose', self.goose) self.assertEqual(self.goose, self.rcache.get_resource_by_id('goose', 1)) self.assertIsNone(self.rcache.get_resource_by_id('goose', 2)) def test__flood_cache_for_query_pulls_once(self): resources = [OVOLikeThing(66), OVOLikeThing(67)] received_kw = [] receiver = lambda *a, **k: received_kw.append(k) registry.subscribe(receiver, 'goose', events.AFTER_UPDATE) self._pullmock.bulk_pull.side_effect = [ resources, [resources[0]], [resources[1]], [resources[1]] ] self.rcache._flood_cache_for_query('goose', id=(66, 67), name=('a', 'b')) self._pullmock.bulk_pull.assert_called_once_with( mock.ANY, 'goose', filter_kwargs={'id': (66, 67), 'name': ('a', 'b')}) self._pullmock.bulk_pull.reset_mock() self.rcache._flood_cache_for_query('goose', id=(66, ), name=('a', )) self.assertFalse(self._pullmock.called) self.rcache._flood_cache_for_query('goose', id=(67, ), name=('b', )) self.assertFalse(self._pullmock.called) # querying by just ID should trigger a new call since ID+name is a more # specific query self.rcache._flood_cache_for_query('goose', id=(67, )) self._pullmock.bulk_pull.assert_called_once_with( mock.ANY, 'goose', filter_kwargs={'id': (67, )}) self.assertItemsEqual( resources, [rec['updated'] for rec in received_kw]) def test_bulk_pull_doesnt_wipe_out_newer_data(self): self.rcache.record_resource_update( self.ctx, 'goose', OVOLikeThing(1, revision_number=5)) updated = OVOLikeThing(1) updated.revision_number = 1 # older revision number self._pullmock.bulk_pull.return_value = [updated] self.rcache._flood_cache_for_query('goose', id=(1,),) self.assertEqual( 5, self.rcache.get_resource_by_id('goose', 1).revision_number) def test_get_resources(self): geese = [OVOLikeThing(3, size='large'), OVOLikeThing(5, size='medium'), OVOLikeThing(4, size='large'), OVOLikeThing(6, size='small')] for goose in geese: self.rcache.record_resource_update(self.ctx, 'goose', goose) is_large = {'size': ('large', )} is_small = {'size': ('small', )} self.assertItemsEqual([geese[0], geese[2]], self.rcache.get_resources('goose', is_large)) self.assertItemsEqual([geese[3]], self.rcache.get_resources('goose', is_small)) def test_match_resources_with_func(self): geese = [OVOLikeThing(3, size='large'), OVOLikeThing(5, size='medium'), OVOLikeThing(4, size='xlarge'), OVOLikeThing(6, size='small')] for goose in geese: self.rcache.record_resource_update(self.ctx, 'goose', goose) has_large = lambda o: 'large' in o.size self.assertItemsEqual([geese[0], geese[2]], self.rcache.match_resources_with_func('goose', has_large)) def test__is_stale(self): goose = OVOLikeThing(3, size='large') self.rcache.record_resource_update(self.ctx, 'goose', goose) # same revision id is not considered stale updated = OVOLikeThing(3, size='large') self.assertFalse(self.rcache._is_stale('goose', updated)) updated.revision_number = 0 self.assertTrue(self.rcache._is_stale('goose', updated)) updated.revision_number = 200 self.assertFalse(self.rcache._is_stale('goose', updated)) # once deleted, all updates are stale self.rcache.record_resource_delete(self.ctx, 'goose', 3) self.assertTrue(self.rcache._is_stale('goose', updated)) def test_record_resource_update(self): received_kw = [] receiver = lambda *a, **k: received_kw.append(k) registry.subscribe(receiver, 'goose', events.AFTER_UPDATE) self.rcache.record_resource_update(self.ctx, 'goose', OVOLikeThing(3, size='large')) self.assertEqual(1, len(received_kw)) self.assertIsNone(received_kw[0]['existing']) # another update with no changed fields results in no callback self.rcache.record_resource_update(self.ctx, 'goose', OVOLikeThing(3, size='large', revision_number=100)) self.assertEqual(1, len(received_kw)) self.rcache.record_resource_update(self.ctx, 'goose', OVOLikeThing(3, size='small', revision_number=101)) self.assertEqual(2, len(received_kw)) self.assertEqual('large', received_kw[1]['existing'].size) self.assertEqual('small', received_kw[1]['updated'].size) self.assertEqual(set(['size']), received_kw[1]['changed_fields']) def test_record_resource_delete(self): received_kw = [] receiver = lambda *a, **k: received_kw.append(k) registry.subscribe(receiver, 'goose', events.AFTER_DELETE) self.rcache.record_resource_update(self.ctx, 'goose', OVOLikeThing(3, size='large')) self.rcache.record_resource_delete(self.ctx, 'goose', 3) self.assertEqual(1, len(received_kw)) self.assertEqual(3, received_kw[0]['existing'].id) self.assertEqual(3, received_kw[0]['resource_id']) # deletes of non-existing cache items are still honored self.rcache.record_resource_delete(self.ctx, 'goose', 4) self.assertEqual(2, len(received_kw)) self.assertIsNone(received_kw[1]['existing']) self.assertEqual(4, received_kw[1]['resource_id']) def test_record_resource_delete_ignores_dups(self): received_kw = [] receiver = lambda *a, **k: received_kw.append(k) registry.subscribe(receiver, 'goose', events.AFTER_DELETE) self.rcache.record_resource_delete(self.ctx, 'goose', 3) self.assertEqual(1, len(received_kw)) self.rcache.record_resource_delete(self.ctx, 'goose', 4) self.assertEqual(2, len(received_kw)) self.rcache.record_resource_delete(self.ctx, 'goose', 3) self.assertEqual(2, len(received_kw)) def test_resource_change_handler(self): with mock.patch.object(resource_cache.RemoteResourceWatcher, '_init_rpc_listeners'): watch = resource_cache.RemoteResourceWatcher(self.rcache) geese = [OVOLikeThing(3, size='large'), OVOLikeThing(5, size='medium'), OVOLikeThing(4, size='large'), OVOLikeThing(6, size='small')] watch.resource_change_handler(self.ctx, 'goose', geese, events_rpc.UPDATED) for goose in geese: self.assertEqual(goose, self.rcache.get_resource_by_id('goose', goose.id)) watch.resource_change_handler(self.ctx, 'goose', geese, events_rpc.DELETED) for goose in geese: self.assertIsNone( self.rcache.get_resource_by_id('goose', goose.id))