def test_get_resources(self): resources = [ { 'identifier': '1', 'name': 'pool-1', }, { 'identifier': '2', 'name': 'pool-2', }, ] response = mock.MagicMock(status_code=200) response.json.return_value = resources self.mock_request.return_value = response self.assertCountEqual(self.client.get_resources('resource-pool'), [ Resource(identifier='1', name='pool-1'), Resource(identifier='2', name='pool-2'), ]) self.assertThat( self.mock_request, MockCalledOnceWith('GET', 'https://rbac.example.com/api/' 'service/1.0/resources/resource-pool', auth=mock.ANY, cookies=mock.ANY, json=None))
def test__rbacDifference(self): service = self.make_service(sentinel.listener) changes = [ RBACSync(action=RBAC_ACTION.UPDATE, resource_id=1, resource_name='r-1'), RBACSync(action=RBAC_ACTION.ADD, resource_id=2, resource_name='r-2'), RBACSync(action=RBAC_ACTION.UPDATE, resource_id=3, resource_name='r-3'), RBACSync(action=RBAC_ACTION.REMOVE, resource_id=1, resource_name='r-1'), RBACSync(action=RBAC_ACTION.UPDATE, resource_id=3, resource_name='r-3-updated'), RBACSync(action=RBAC_ACTION.ADD, resource_id=4, resource_name='r-4'), RBACSync(action=RBAC_ACTION.REMOVE, resource_id=4, resource_name='r-4'), ] self.assertEquals(([ Resource(identifier=2, name='r-2'), Resource(identifier=3, name='r-3-updated'), ], { 1, }), service._rbacDifference(changes))
def test_get_resources(self): resources = [ { "identifier": "1", "name": "pool-1" }, { "identifier": "2", "name": "pool-2" }, ] response = mock.MagicMock(status_code=200) response.json.return_value = resources self.mock_request.return_value = response self.assertCountEqual( self.client.get_resources("resource-pool"), [ Resource(identifier="1", name="pool-1"), Resource(identifier="2", name="pool-2"), ], ) self.assertThat( self.mock_request, MockCalledOnceWith( "GET", "https://rbac.example.com/api/" "service/v1/resources/resource-pool", auth=mock.ANY, cookies=mock.ANY, json=None, ), )
def test_update_resources(self): updates = [ Resource(identifier='1', name='pool-1'), Resource(identifier='2', name='pool-2'), ] removals = [11, 22, 33] json = { 'last-sync-id': 'a-b-c', 'updates': [ { 'identifier': '1', 'name': 'pool-1', }, { 'identifier': '2', 'name': 'pool-2', }, ], 'removals': ['11', '22', '33'] } response = mock.MagicMock(status_code=200) response.json.return_value = {'sync-id': 'x-y-z'} self.mock_request.return_value = response sync_id = self.client.update_resources( 'resource-pool', updates=updates, removals=removals, last_sync_id='a-b-c') self.assertEqual(sync_id, 'x-y-z') self.assertThat( self.mock_request, MockCalledOnceWith( 'POST', 'https://rbac.example.com/api/' 'service/1.0/resources/resource-pool', auth=mock.ANY, cookies=mock.ANY, json=json))
def test_update_resources_no_sync_id(self): updates = [ Resource(identifier="1", name="pool-1"), Resource(identifier="2", name="pool-2"), ] removals = [11, 22, 33] # removals are ignored json = { "last-sync-id": None, "updates": [ {"identifier": "1", "name": "pool-1"}, {"identifier": "2", "name": "pool-2"}, ], "removals": [], } response = mock.MagicMock(status_code=200) response.json.return_value = {"sync-id": "x-y-z"} self.mock_request.return_value = response sync_id = self.client.update_resources( "resource-pool", updates=updates, removals=removals ) self.assertEqual(sync_id, "x-y-z") self.assertThat( self.mock_request, MockCalledOnceWith( "POST", "https://rbac.example.com/api/" "service/v1/resources/resource-pool", auth=mock.ANY, cookies=mock.ANY, json=json, ), )
def test_update_resources_sync_conflict(self): updates = [ Resource(identifier='1', name='pool-1'), Resource(identifier='2', name='pool-2'), ] removals = [11, 22, 33] response = mock.MagicMock(status_code=int(http.client.CONFLICT)) response.json.return_value = {'sync-id': 'x-y-z'} self.mock_request.return_value = response self.assertRaises(SyncConflictError, self.client.update_resources, 'resource-pool', updates=updates, removals=removals, last_sync_id='a-b-c')
def _rbacDifference(self, changes): """Return the only the changes that need to be pushed to RBAC.""" # Removals are calculated first. A removal is never followed by an # update and `resource_id` is never re-used. removals = set(change.resource_id for change in changes if change.action == RBAC_ACTION.REMOVE) # Changes are ordered from oldest to lates. The latest change will # be the last item of that `resource_id` in the dictionary. updates = { change.resource_id: change.resource_name for change in changes if (change.resource_id not in removals and change.action != RBAC_ACTION.REMOVE) } # Any additions with also a removal is not sync to RBAC. for change in changes: if change.action == RBAC_ACTION.ADD: if change.resource_id in removals: removals.remove(change.resource_id) return ( sorted( [ Resource(identifier=res_id, name=res_name) for res_id, res_name in updates.items() ], key=attrgetter("identifier"), ), removals, )
def make_resource_pools(self): rpools = [factory.make_ResourcePool() for _ in range(3)] return rpools, sorted([ Resource(identifier=rpool.id, name=rpool.name) for rpool in ResourcePool.objects.all() ], key=attrgetter('identifier'))
def test_update_resources_sync_conflict(self): updates = [ Resource(identifier="1", name="pool-1"), Resource(identifier="2", name="pool-2"), ] removals = [11, 22, 33] response = mock.MagicMock(status_code=int(http.client.CONFLICT)) response.json.return_value = {"sync-id": "x-y-z"} self.mock_request.return_value = response self.assertRaises( SyncConflictError, self.client.update_resources, "resource-pool", updates=updates, removals=removals, last_sync_id="a-b-c", )
def _rbacSync(self): """Sync the RBAC information.""" changes = RBACSync.objects.changes() if not changes and self.rbacInit: # Nothing has changed, meaning another region already took care # of performing the update. return None client = self._getRBACClient() if client is None: # RBAC is disabled, do nothing. RBACSync.objects.clear() # Changes not needed, RBAC disabled. return None # XXX blake_r 2018-10-04 - This can be smarter by looking at the action # and resource_type information in the changes and include the # last_sync_id in the request, to only update what changed. resources = [ Resource(identifier=rpool.id, name=rpool.name) for rpool in ResourcePool.objects.all() ] new_sync_id = client.update_resources('resource-pool', updates=resources) RBACLastSync.objects.update_or_create( resource_type='resource-pool', defaults={'sync_id': new_sync_id}) if not self.rbacInit: # This was initial sync on start-up. RBACSync.objects.clear() return [] # Return the changes and clear the table, so new changes will be # tracked. Being inside a transaction allows us not to worry about # a new change already existing with the clear. changes = [change.source for change in changes] RBACSync.objects.clear() return changes
def _rbacSync(self): """Sync the RBAC information.""" # Currently this whole method is scoped to dealing with # 'resource-pool'. As more items are synced to RBAC this # will need to be adjusted to handle multiple. changes = RBACSync.objects.changes('resource-pool') if not changes and self.rbacInit: # Nothing has changed, meaning another region already took care # of performing the update. return None client = self._getRBACClient() if client is None: # RBAC is disabled, do nothing. RBACSync.objects.clear('resource-pool') # Changes not needed. return None # Push the resource information based on the last sync. new_sync_id = None try: last_sync = RBACLastSync.objects.get(resource_type='resource-pool') except RBACLastSync.DoesNotExist: last_sync = None if last_sync is None or self._rbacNeedsFull(changes): # First sync or requires a full sync. resources = [ Resource(identifier=rpool.id, name=rpool.name) for rpool in ResourcePool.objects.order_by('id') ] new_sync_id = client.update_resources('resource-pool', updates=resources) else: # Send only the difference of what has been changed. updates, removals = self._rbacDifference(changes) if updates or removals: try: new_sync_id = client.update_resources( 'resource-pool', updates=updates, removals=removals, last_sync_id=last_sync.sync_id) except SyncConflictError: # Issue occurred syncing, push all information. resources = [ Resource(identifier=rpool.id, name=rpool.name) for rpool in ResourcePool.objects.order_by('id') ] new_sync_id = client.update_resources('resource-pool', updates=resources) if new_sync_id: RBACLastSync.objects.update_or_create( resource_type='resource-pool', defaults={'sync_id': new_sync_id}) if not self.rbacInit: # This was initial sync on start-up. RBACSync.objects.clear('resource-pool') return [] # Return the changes and clear the table, so new changes will be # tracked. Being inside a transaction allows us not to worry about # a new change already existing with the clear. changes = [change.source for change in changes] RBACSync.objects.clear('resource-pool') return changes
def make_resource_pools(self): rpools = [factory.make_ResourcePool() for _ in range(3)] return rpools, [ Resource(identifier=rpool.id, name=rpool.name) for rpool in ResourcePool.objects.all() ]