Fix cross-project incremental backups

This patch addresses the scenario where an
incremental backup can be created having a
parent backup that was taken in a different
project. This scenario ultimately leads to
a silent error when creating/deleting the
incremental backup and quotas going out of
sync.

The proposed fix is to narrow the backup search
down to the same project. To achieve this, a
method's signature had to be updated to achieve
the desired optimized behavior of passing the
volume's project_id parameter.

Closes-bug: #1869746
Closes-bug: #1873518

Change-Id: Icb621ff5966133f59d9d43ca2dd9f8e1919b1149
(cherry picked from commit 8ebeafcbba)
This commit is contained in:
Rodrigo Barbieri 2020-07-01 18:20:15 -03:00
parent 0a74d5b49f
commit 5358c996b4
8 changed files with 44 additions and 27 deletions

View File

@ -243,8 +243,9 @@ class API(base.Base):
# incremental backup. # incremental backup.
latest_backup = None latest_backup = None
if incremental: if incremental:
backups = objects.BackupList.get_all_by_volume(context.elevated(), backups = objects.BackupList.get_all_by_volume(
volume_id) context, volume_id, volume['project_id'],
filters={'project_id': context.project_id})
if backups.objects: if backups.objects:
# NOTE(xyang): The 'data_timestamp' field records the time # NOTE(xyang): The 'data_timestamp' field records the time
# when the data on the volume was first saved. If it is # when the data on the volume was first saved. If it is

View File

@ -1232,9 +1232,9 @@ def backup_get_all_by_project(context, project_id, filters=None, marker=None,
sort_dirs=sort_dirs) sort_dirs=sort_dirs)
def backup_get_all_by_volume(context, volume_id, filters=None): def backup_get_all_by_volume(context, volume_id, vol_project_id, filters=None):
"""Get all backups belonging to a volume.""" """Get all backups belonging to a volume."""
return IMPL.backup_get_all_by_volume(context, volume_id, return IMPL.backup_get_all_by_volume(context, volume_id, vol_project_id,
filters=filters) filters=filters)

View File

@ -5268,9 +5268,9 @@ def backup_get_all_by_project(context, project_id, filters=None, marker=None,
@require_context @require_context
def backup_get_all_by_volume(context, volume_id, filters=None): def backup_get_all_by_volume(context, volume_id, vol_project_id, filters=None):
authorize_project_context(context, volume_id) authorize_project_context(context, vol_project_id)
if not filters: if not filters:
filters = {} filters = {}
else: else:

View File

@ -261,8 +261,10 @@ class BackupList(base.ObjectListBase, base.CinderObject):
backups, expected_attrs=expected_attrs) backups, expected_attrs=expected_attrs)
@classmethod @classmethod
def get_all_by_volume(cls, context, volume_id, filters=None): def get_all_by_volume(
backups = db.backup_get_all_by_volume(context, volume_id, filters) cls, context, volume_id, vol_project_id, filters=None):
backups = db.backup_get_all_by_volume(
context, volume_id, vol_project_id, filters)
expected_attrs = Backup._get_expected_attrs(context) expected_attrs = Backup._get_expected_attrs(context)
return base.obj_make_list(context, cls(context), objects.Backup, return base.obj_make_list(context, cls(context), objects.Backup,
backups, expected_attrs=expected_attrs) backups, expected_attrs=expected_attrs)

View File

@ -124,7 +124,8 @@ class BaseBackupTest(test.TestCase):
previous_status='available', previous_status='available',
size=1, size=1,
host='testhost', host='testhost',
encryption_key_id=None): encryption_key_id=None,
project_id=None):
"""Create a volume entry in the DB. """Create a volume entry in the DB.
Return the entry ID Return the entry ID
@ -132,8 +133,8 @@ class BaseBackupTest(test.TestCase):
vol = {} vol = {}
vol['size'] = size vol['size'] = size
vol['host'] = host vol['host'] = host
vol['user_id'] = str(uuid.uuid4()) vol['user_id'] = fake.USER_ID
vol['project_id'] = str(uuid.uuid4()) vol['project_id'] = project_id or fake.PROJECT_ID
vol['status'] = status vol['status'] = status
vol['display_name'] = display_name vol['display_name'] = display_name
vol['display_description'] = display_description vol['display_description'] = display_description
@ -2058,19 +2059,14 @@ class BackupAPITestCase(BaseBackupTest):
self.ctxt, backups=1, backup_gigabytes=1) self.ctxt, backups=1, backup_gigabytes=1)
mock_rollback.assert_called_with(self.ctxt, "fake_reservation") mock_rollback.assert_called_with(self.ctxt, "fake_reservation")
@mock.patch('cinder.db.backup_get_all_by_volume') @mock.patch('cinder.objects.BackupList.get_all_by_volume')
@mock.patch('cinder.backup.rpcapi.BackupAPI.create_backup')
@mock.patch.object(api.API, '_get_available_backup_service_host',
return_value='fake_host')
@mock.patch.object(quota.QUOTAS, 'rollback') @mock.patch.object(quota.QUOTAS, 'rollback')
@mock.patch.object(quota.QUOTAS, 'reserve') @mock.patch.object(quota.QUOTAS, 'reserve')
def test_create_backup_failed_with_empty_backup_objects( def test_create_backup_failed_with_empty_backup_objects(
self, mock_reserve, mock_rollback, mock_get_service, self, mock_reserve, mock_rollback, mock_get_backups):
mock_create, mock_get_backups): backups = mock.Mock()
mock_get_backups.return_value = [v2_fakes.fake_backup('fake-1')]
backups = objects.BackupList.get_all_by_volume(self.ctxt,
fake.VOLUME_ID)
backups.objects = [] backups.objects = []
mock_get_backups.return_value = backups
is_incremental = True is_incremental = True
self.ctxt.user_id = 'fake_user' self.ctxt.user_id = 'fake_user'
self.ctxt.project_id = 'fake_project' self.ctxt.project_id = 'fake_project'
@ -2078,7 +2074,8 @@ class BackupAPITestCase(BaseBackupTest):
volume_id = self._create_volume_db_entry(status='available', volume_id = self._create_volume_db_entry(status='available',
host='testhost#rbd', host='testhost#rbd',
size=1) size=1,
project_id="vol_proj_id")
self.assertRaises(exception.InvalidBackup, self.assertRaises(exception.InvalidBackup,
self.api.create, self.api.create,
self.ctxt, self.ctxt,
@ -2086,6 +2083,9 @@ class BackupAPITestCase(BaseBackupTest):
volume_id, None, volume_id, None,
incremental=is_incremental) incremental=is_incremental)
mock_rollback.assert_called_with(self.ctxt, "fake_reservation") mock_rollback.assert_called_with(self.ctxt, "fake_reservation")
mock_get_backups.assert_called_once_with(
self.ctxt, volume_id, 'vol_proj_id',
filters={'project_id': 'fake_project'})
@mock.patch('cinder.db.backup_get_all_by_volume', @mock.patch('cinder.db.backup_get_all_by_volume',
return_value=[v2_fakes.fake_backup('fake-1')]) return_value=[v2_fakes.fake_backup('fake-1')])

View File

@ -290,11 +290,13 @@ class TestBackupList(test_objects.BaseObjectsTestCase):
@mock.patch('cinder.db.backup_get_all_by_volume', @mock.patch('cinder.db.backup_get_all_by_volume',
return_value=[fake_backup]) return_value=[fake_backup])
def test_get_all_by_volume(self, get_all_by_volume): def test_get_all_by_volume(self, get_all_by_volume):
backups = objects.BackupList.get_all_by_volume(self.context, backups = objects.BackupList.get_all_by_volume(
fake.VOLUME_ID) self.context, fake.VOLUME_ID, 'fake_proj')
self.assertEqual(1, len(backups)) self.assertEqual(1, len(backups))
get_all_by_volume.assert_called_once_with(self.context, get_all_by_volume.assert_called_once_with(self.context,
fake.VOLUME_ID, None) fake.VOLUME_ID,
'fake_proj',
None)
TestBackup._compare(self, fake_backup, backups[0]) TestBackup._compare(self, fake_backup, backups[0])

View File

@ -2879,15 +2879,21 @@ class DBAPIBackupTestCase(BaseTest):
{'fake_key': 'fake'}) {'fake_key': 'fake'})
self._assertEqualListsOfObjects([], byproj) self._assertEqualListsOfObjects([], byproj)
def test_backup_get_all_by_volume(self): @mock.patch.object(sqlalchemy_api, 'authorize_project_context')
byvol = db.backup_get_all_by_volume(self.ctxt, def test_backup_get_all_by_volume(self, mock_authorize):
self.created[1]['volume_id']) byvol = db.backup_get_all_by_volume(
self.ctxt, self.created[1]['volume_id'], 'fake_proj')
self._assertEqualObjects(self.created[1], byvol[0]) self._assertEqualObjects(self.created[1], byvol[0])
byvol = db.backup_get_all_by_volume(self.ctxt, byvol = db.backup_get_all_by_volume(self.ctxt,
self.created[1]['volume_id'], self.created[1]['volume_id'],
'fake_proj',
{'fake_key': 'fake'}) {'fake_key': 'fake'})
self._assertEqualListsOfObjects([], byvol) self._assertEqualListsOfObjects([], byvol)
mock_authorize.assert_has_calls([
mock.call(self.ctxt, 'fake_proj'),
mock.call(self.ctxt, 'fake_proj')
])
def test_backup_update_nonexistent(self): def test_backup_update_nonexistent(self):
self.assertRaises(exception.BackupNotFound, self.assertRaises(exception.BackupNotFound,

View File

@ -0,0 +1,6 @@
---
fixes:
- |
Cinder no longer allows an incremental backup to be
created while having the parent backup in another
project.