Merge "NFS driver: Set volume format for snapshot operations"
This commit is contained in:
@@ -221,6 +221,20 @@ def qemu_img_info(
|
||||
return info
|
||||
|
||||
|
||||
def get_image_format(path: str,
|
||||
allow_qcow2_backing_file: Optional[bool] = False
|
||||
) -> Optional[str]:
|
||||
"""Inspect image file and return its format if safe.
|
||||
|
||||
:param path: Path to the image file to inspect
|
||||
:param allow_qcow2_backing_file: Allow qcow2 images with backing files
|
||||
:returns: Format name ('raw', 'qcow2', etc.) if safe, None if unsafe
|
||||
"""
|
||||
return cinder.privsep.format_inspector.get_format_if_safe(
|
||||
path=path,
|
||||
allow_qcow2_backing_file=allow_qcow2_backing_file)
|
||||
|
||||
|
||||
def get_qemu_img_version() -> Optional[list[int]]:
|
||||
"""The qemu-img version will be cached until the process is restarted."""
|
||||
|
||||
|
||||
@@ -2409,6 +2409,23 @@ class TestTemporaryFileContextManager(test.TestCase):
|
||||
mock_delete.assert_called_once_with(mock.sentinel.temporary_file)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestGetImageFormat(test.TestCase):
|
||||
@ddt.data(False, True)
|
||||
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
|
||||
def test_get_image_format(self, allow_qcow2_backing_file,
|
||||
mock_get_format_if_safe):
|
||||
fake_path = mock.sentinel.fake_path
|
||||
mock_get_format_if_safe.return_value = 'qcow2'
|
||||
|
||||
result = image_utils.get_image_format(
|
||||
fake_path, allow_qcow2_backing_file=allow_qcow2_backing_file)
|
||||
|
||||
self.assertEqual('qcow2', result)
|
||||
mock_get_format_if_safe.assert_called_once_with(
|
||||
path=fake_path, allow_qcow2_backing_file=allow_qcow2_backing_file)
|
||||
|
||||
|
||||
class TestImageUtils(test.TestCase):
|
||||
def test_get_virtual_size(self):
|
||||
image_id = fake.IMAGE_ID
|
||||
|
||||
@@ -1561,6 +1561,7 @@ class NfsDriverTestCase(test.TestCase):
|
||||
self._set_driver()
|
||||
drv = self._driver
|
||||
volume = self._simple_volume()
|
||||
volume.admin_metadata = {'format': 'raw'}
|
||||
self.override_config('nfs_snapshot_support', True)
|
||||
fake_snap = fake_snapshot.fake_snapshot_obj(self.context)
|
||||
fake_snap.volume = volume
|
||||
@@ -1570,6 +1571,7 @@ class NfsDriverTestCase(test.TestCase):
|
||||
snap_path = os.path.join(vol_dir, snap_file)
|
||||
info_path = os.path.join(vol_dir, volume['name']) + '.info'
|
||||
|
||||
mock_vol_save = self.mock_object(fake_snap.volume, 'save')
|
||||
with mock.patch.object(drv, '_local_path_volume_info',
|
||||
return_value=info_path), \
|
||||
mock.patch.object(drv, '_read_info_file', return_value={}), \
|
||||
@@ -1582,12 +1584,15 @@ class NfsDriverTestCase(test.TestCase):
|
||||
mock.patch.object(drv, 'get_active_image_from_info',
|
||||
return_value=volume['name']), \
|
||||
mock.patch.object(drv, '_get_new_snap_path',
|
||||
return_value=snap_path):
|
||||
return_value=snap_path), \
|
||||
mock.patch.object(fake_snap.volume, 'save') \
|
||||
as mock_vol_save:
|
||||
self._driver.create_snapshot(fake_snap)
|
||||
|
||||
mock_check_support.assert_called_once()
|
||||
mock_do_create_snapshot.assert_called_with(fake_snap, volume['name'],
|
||||
snap_path)
|
||||
mock_vol_save.assert_called_once()
|
||||
mock_write_info_file.assert_called_with(
|
||||
info_path, {'active': snap_file, fake_snap.id: snap_file})
|
||||
|
||||
|
||||
@@ -733,10 +733,12 @@ class QuobyteDriverTestCase(test.TestCase):
|
||||
|
||||
drv._create_sparsed_file = mock.Mock()
|
||||
drv._set_rw_permissions_for_all = mock.Mock()
|
||||
mock_volume_save = self.mock_object(volume, 'save')
|
||||
|
||||
drv._do_create_volume(volume)
|
||||
drv._create_sparsed_file.assert_called_once_with(mock.ANY, mock.ANY)
|
||||
drv._set_rw_permissions_for_all.assert_called_once_with(mock.ANY)
|
||||
mock_volume_save.assert_called_once()
|
||||
|
||||
def test_create_nonsparsed_volume(self):
|
||||
drv = self._driver
|
||||
@@ -747,10 +749,12 @@ class QuobyteDriverTestCase(test.TestCase):
|
||||
|
||||
drv._create_regular_file = mock.Mock()
|
||||
drv._set_rw_permissions_for_all = mock.Mock()
|
||||
mock_volume_save = self.mock_object(volume, 'save')
|
||||
|
||||
drv._do_create_volume(volume)
|
||||
drv._create_regular_file.assert_called_once_with(mock.ANY, mock.ANY)
|
||||
drv._set_rw_permissions_for_all.assert_called_once_with(mock.ANY)
|
||||
mock_volume_save.assert_called_once()
|
||||
|
||||
self._configuration.quobyte_sparsed_volumes = old_value
|
||||
|
||||
@@ -761,6 +765,7 @@ class QuobyteDriverTestCase(test.TestCase):
|
||||
old_value = self._configuration.quobyte_qcow2_volumes
|
||||
self._configuration.quobyte_qcow2_volumes = True
|
||||
|
||||
mock_volume_save = self.mock_object(volume, 'save')
|
||||
drv._execute = mock.Mock()
|
||||
|
||||
hashed = drv._get_hash_str(volume['provider_location'])
|
||||
@@ -777,6 +782,7 @@ class QuobyteDriverTestCase(test.TestCase):
|
||||
mock.call('chmod', 'ugo+rw', path,
|
||||
run_as_root=self._driver._execute_as_root)]
|
||||
drv._execute.assert_has_calls(assert_calls)
|
||||
mock_volume_save.assert_called_once()
|
||||
|
||||
self._configuration.quobyte_qcow2_volumes = old_value
|
||||
|
||||
|
||||
@@ -98,7 +98,10 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
stale_snapshot=False,
|
||||
is_active_image=True,
|
||||
is_tmp_snap=False,
|
||||
encryption=False):
|
||||
encryption=False,
|
||||
base_format_exists=True,
|
||||
base_file_exists=True,
|
||||
base_file_inspect_format=None):
|
||||
# If the snapshot is not the active image, it is guaranteed that
|
||||
# another snapshot exists having it as backing file.
|
||||
|
||||
@@ -135,6 +138,7 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
else:
|
||||
fake_snap_img_info.backing_file = volume_name
|
||||
fake_snap_img_info.file_format = 'qcow2'
|
||||
fake_snap_img_info.backing_file_format = 'raw'
|
||||
fake_base_img_info.backing_file = None
|
||||
fake_base_img_info.file_format = 'raw'
|
||||
|
||||
@@ -156,6 +160,12 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
exp_acceptable_states = ['available', 'in-use', 'backing-up',
|
||||
'deleting', 'downloading']
|
||||
|
||||
if base_format_exists:
|
||||
snapshot.volume.admin_metadata = {'format': 'qcow2',
|
||||
'base_format': 'raw'}
|
||||
else:
|
||||
snapshot.volume.admin_metadata = {'format': 'qcow2'}
|
||||
|
||||
if volume_in_use:
|
||||
snapshot.volume.status = 'backing-up'
|
||||
snapshot.volume.attach_status = 'attached'
|
||||
@@ -183,9 +193,61 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
|
||||
elif is_active_image:
|
||||
self._driver._read_info_file.return_value = fake_info
|
||||
mock_vol_save = self.mock_object(snapshot.volume, 'save')
|
||||
mock_exists = self.mock_object(os.path, 'exists')
|
||||
mock_get_image_format = self.mock_object(
|
||||
image_utils, 'get_image_format')
|
||||
|
||||
mock_exists.return_value = base_file_exists
|
||||
mock_get_image_format.return_value = base_file_inspect_format
|
||||
|
||||
self._driver._delete_snapshot(snapshot)
|
||||
|
||||
if not base_format_exists:
|
||||
# When base_format does not exist, we expect the function to
|
||||
# try recoverying the information based on the state of
|
||||
# volume's base file.
|
||||
base_path = os.path.join(self._FAKE_MNT_POINT, volume_name)
|
||||
mock_exists.assert_called_once_with(base_path)
|
||||
|
||||
if base_file_exists:
|
||||
# If base file exists, get_image_format is expected to be
|
||||
# called for detecting the base file format.
|
||||
mock_get_image_format.assert_called_once_with(
|
||||
path=base_path, allow_qcow2_backing_file=True)
|
||||
|
||||
if base_file_inspect_format == 'raw':
|
||||
# When format inspection returns 'raw', the
|
||||
# base_format should be set to 'raw'.
|
||||
self.assertEqual(
|
||||
'raw',
|
||||
snapshot.volume.admin_metadata['base_format'])
|
||||
self.assertEqual(
|
||||
'raw',
|
||||
snapshot.volume.admin_metadata['format'])
|
||||
mock_vol_save.assert_called_once()
|
||||
else:
|
||||
# If inspection fails or returns non-raw (likely
|
||||
# 'qcow2'), it isn't possible to reliably
|
||||
# determine the original format. For this reason,
|
||||
# metadata should NOT be updated.
|
||||
self.assertNotIn(
|
||||
'base_format', snapshot.volume.admin_metadata)
|
||||
# Format should still be qcow2 as before
|
||||
self.assertEqual(
|
||||
'qcow2', snapshot.volume.admin_metadata['format'])
|
||||
# "save" should NOT be called since metadata wasn't
|
||||
# suposed to be updated.
|
||||
mock_vol_save.assert_not_called()
|
||||
else:
|
||||
# When base_format is missing and base file doesn't exist,
|
||||
# verify base_format and format were set to 'qcow2'.
|
||||
self.assertEqual(
|
||||
'qcow2', snapshot.volume.admin_metadata['base_format'])
|
||||
self.assertEqual(
|
||||
'qcow2', snapshot.volume.admin_metadata['format'])
|
||||
mock_vol_save.assert_called_once()
|
||||
|
||||
self._driver._img_commit.assert_called_once_with(
|
||||
snapshot_path)
|
||||
self.assertNotIn(snapshot.id, fake_info)
|
||||
@@ -217,9 +279,10 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
|
||||
self._driver._img_commit.assert_called_once_with(
|
||||
snapshot_path)
|
||||
self._driver._rebase_img.assert_called_once_with(
|
||||
fake_upper_snap_path, volume_name,
|
||||
fake_base_img_info.file_format)
|
||||
if volume_in_use:
|
||||
self._driver._rebase_img.assert_called_once_with(
|
||||
fake_upper_snap_path, volume_name,
|
||||
fake_base_img_info.file_format)
|
||||
self._driver._write_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, expected_info)
|
||||
|
||||
@@ -245,6 +308,45 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._test_delete_snapshot(is_active_image=False,
|
||||
encryption=encryption)
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_base_format_recovery_base_not_exists(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(
|
||||
is_active_image=True,
|
||||
encryption=encryption,
|
||||
base_format_exists=False,
|
||||
base_file_exists=False)
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_base_format_recovery_inspect_raw(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(
|
||||
is_active_image=True,
|
||||
encryption=encryption,
|
||||
base_format_exists=False,
|
||||
base_file_exists=True,
|
||||
base_file_inspect_format='raw')
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_base_format_recovery_inspect_qcow2(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(
|
||||
is_active_image=True,
|
||||
encryption=encryption,
|
||||
base_format_exists=False,
|
||||
base_file_exists=True,
|
||||
base_file_inspect_format='qcow2')
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_snapshot_base_format_recovery_inspect_none(self,
|
||||
encryption):
|
||||
self._test_delete_snapshot(
|
||||
is_active_image=True,
|
||||
encryption=encryption,
|
||||
base_format_exists=False,
|
||||
base_file_exists=True,
|
||||
base_file_inspect_format=None)
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_delete_stale_snapshot(self, encryption):
|
||||
if encryption:
|
||||
@@ -278,6 +380,41 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self._driver._write_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, expected_info)
|
||||
|
||||
@ddt.data({'snap_info': {'active': 'snap2', 'snap1': 'snap1',
|
||||
'snap2': 'snap2'},
|
||||
'expected_snapshot_ids': ['snap1', 'snap2']},
|
||||
{'snap_info': {},
|
||||
'expected_snapshot_ids': []})
|
||||
@ddt.unpack
|
||||
def test_get_snapshots_from_snap_info(self, snap_info,
|
||||
expected_snapshot_ids):
|
||||
result = self._driver._get_snapshots_from_snap_info(snap_info)
|
||||
self.assertEqual(expected_snapshot_ids, result)
|
||||
|
||||
@ddt.data({'read_info': {'active': 'snap2', 'snap1': 'snap1',
|
||||
'snap2': 'snap2'},
|
||||
'expected_snapshot_ids': ['snap1', 'snap2']},
|
||||
{'read_info': {},
|
||||
'expected_snapshot_ids': []})
|
||||
@ddt.unpack
|
||||
@mock.patch.object(remotefs.RemoteFSSnapDriver, '_read_info_file')
|
||||
@mock.patch.object(remotefs.RemoteFSSnapDriver, '_local_path_volume_info')
|
||||
def test_get_volume_snapshots(self, mock_local_path_info,
|
||||
mock_read_info_file, read_info,
|
||||
expected_snapshot_ids):
|
||||
|
||||
mock_volume = mock.sentinel.volume
|
||||
mock_local_path_info.return_value = mock.sentinel.fake_info_path
|
||||
mock_read_info_file.return_value = read_info
|
||||
|
||||
result = self._driver._get_volume_snapshots(volume=mock_volume)
|
||||
|
||||
self.assertEqual(expected_snapshot_ids, result)
|
||||
|
||||
mock_local_path_info.assert_called_once_with(mock_volume)
|
||||
mock_read_info_file.assert_called_once_with(
|
||||
mock.sentinel.fake_info_path, empty_if_missing=True)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSDriver,
|
||||
'secure_file_operations_enabled',
|
||||
return_value=True)
|
||||
@@ -352,6 +489,8 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
exp_acceptable_states.append('downloading')
|
||||
self._fake_snapshot.volume.status = 'downloading'
|
||||
|
||||
mock_vol_save = self.mock_object(snapshot.volume, 'save')
|
||||
snapshot.volume.admin_metadata = {'format': 'raw'}
|
||||
if volume_in_use:
|
||||
snapshot.volume.status = 'backing-up'
|
||||
snapshot.volume.attach_status = 'attached'
|
||||
@@ -363,7 +502,6 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
self.context, connection_info=conn_info)
|
||||
snapshot.volume.volume_attachment.objects.append(attachment)
|
||||
mock_save = self.mock_object(attachment, 'save')
|
||||
mock_vol_save = self.mock_object(snapshot.volume, 'save')
|
||||
|
||||
# After the snapshot the connection info should change the name of
|
||||
# the file
|
||||
@@ -389,9 +527,9 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
if volume_in_use:
|
||||
mock_save.assert_called_once()
|
||||
# We should have updated the volume format after the snapshot
|
||||
mock_vol_save.assert_called_once()
|
||||
changed_fields = attachment.cinder_obj_get_changes()
|
||||
self.assertEqual(expected, changed_fields['connection_info'])
|
||||
mock_vol_save.assert_called_once()
|
||||
|
||||
@ddt.data({'encryption': True}, {'encryption': False})
|
||||
def test_create_snapshot_volume_available(self, encryption):
|
||||
|
||||
@@ -587,6 +587,13 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
|
||||
else:
|
||||
self._create_regular_file(volume_path, volume_size)
|
||||
|
||||
# It is required to store the volume format so the generic Snapshots
|
||||
# implementation can revert to the volume's original format on snapshot
|
||||
# deletion
|
||||
volume.admin_metadata['format'] = self.format
|
||||
with volume.obj_as_admin():
|
||||
volume.save()
|
||||
|
||||
self._set_rw_permissions_for_all(volume_path)
|
||||
|
||||
def _load_shares_config(self, share_file=None):
|
||||
|
||||
@@ -793,6 +793,14 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
|
||||
self._nova = compute.API()
|
||||
|
||||
# Set the expected volume format based on driver configuration.
|
||||
# This is used as a heuristic for base_format recovery during
|
||||
# upgrade scenarios when metadata is missing.
|
||||
self.format = 'raw'
|
||||
if getattr(self.configuration, self.driver_prefix + '_qcow2_volumes',
|
||||
False):
|
||||
self.format = 'qcow2'
|
||||
|
||||
def snapshot_revert_use_temp_snapshot(self) -> bool:
|
||||
# Considering that RemoteFS based drivers use COW images
|
||||
# for storing snapshots, having chains of such images,
|
||||
@@ -1324,30 +1332,34 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
# info file instead of blocking
|
||||
return self._delete_stale_snapshot(snapshot)
|
||||
|
||||
base_path = os.path.join(vol_path, base_file)
|
||||
base_file_img_info = self._qemu_img_info(base_path,
|
||||
snapshot.volume.name)
|
||||
base_id = None
|
||||
snapshots = [(snap_id, snap_file) for snap_id, snap_file
|
||||
in snap_info.items()
|
||||
if snap_id != 'active']
|
||||
for snap_id, snap_file in snapshots:
|
||||
if utils.paths_normcase_equal(snap_file, base_file):
|
||||
base_id = snap_id
|
||||
break
|
||||
|
||||
is_last_snapshot = False
|
||||
if base_id is None:
|
||||
# This snapshot is backed directly by the base volume file,
|
||||
# meaning it's the last remaining snapshot in the chain
|
||||
is_last_snapshot = True
|
||||
|
||||
# Find what file has this as its backing file
|
||||
active_file = self.get_active_image_from_info(snapshot.volume)
|
||||
|
||||
base_path = os.path.join(vol_path, base_file)
|
||||
base_file_img_info = self._qemu_img_info(base_path,
|
||||
snapshot.volume.name)
|
||||
|
||||
if self._is_volume_attached(snapshot.volume):
|
||||
# Online delete
|
||||
context = snapshot._context
|
||||
|
||||
new_base_file = base_file_img_info.backing_file
|
||||
|
||||
base_id = None
|
||||
for key, value in snap_info.items():
|
||||
if utils.paths_normcase_equal(value,
|
||||
base_file) and key != 'active':
|
||||
base_id = key
|
||||
break
|
||||
if base_id is None:
|
||||
# This means we are deleting the oldest snapshot
|
||||
LOG.debug('No %(base_id)s found for %(file)s',
|
||||
{'base_id': 'base_id', 'file': snapshot_file})
|
||||
|
||||
online_delete_info = {
|
||||
'active_file': active_file,
|
||||
'snapshot_file': snapshot_file,
|
||||
@@ -1389,6 +1401,89 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
self._img_commit(snapshot_path)
|
||||
# Active file has changed
|
||||
snap_info['active'] = base_file
|
||||
|
||||
# Restore original format when deleting last snapshot
|
||||
if is_last_snapshot:
|
||||
update_metadata = False
|
||||
with snapshot.volume.obj_as_admin():
|
||||
admin_metadata = snapshot.volume.admin_metadata
|
||||
|
||||
if 'base_format' in admin_metadata:
|
||||
# If base_format exists, update the volume's format
|
||||
# back to its original format
|
||||
base_format = admin_metadata['base_format']
|
||||
admin_metadata['format'] = base_format
|
||||
update_metadata = True
|
||||
else:
|
||||
# Volume has a snapshot but base_format doesn't exist,
|
||||
# this is likely because Cinder was upgraded from a
|
||||
# previous version that did not save the base_format.
|
||||
#
|
||||
# Even though the base file may still exist and
|
||||
# could be inspected to determine its format, the
|
||||
# result might be unreliable when the output differs
|
||||
# from 'raw' format. In case of a 'raw'
|
||||
# volume containing a 'qcow2' image, inspection will
|
||||
# report the inner format 'qcow2' instead of the
|
||||
# expected outer format 'raw'. This would lead to
|
||||
# the base format being set incorrectly in the
|
||||
# database.
|
||||
#
|
||||
# There are two possible ways to recover the original
|
||||
# volume format:
|
||||
#
|
||||
# 1. If the base file still exists, inspect it:
|
||||
# a) If format is 'raw': No qcow2 header found,
|
||||
# we can reliably assume 'raw' format.
|
||||
# b) If format is NOT 'raw' (likely qcow2):
|
||||
# Don't recover since we can't reliably
|
||||
# determine the image format.
|
||||
# 2. If the base file no longer exists, this means
|
||||
# the volume has been merged (originally raw or
|
||||
# qcow2) into the active snapshot on a previous
|
||||
# online snapshot deletion. We can assume 'qcow2'
|
||||
# is the base format.
|
||||
if os.path.exists(base_path):
|
||||
# Base file exists, inspect it to determine format
|
||||
inspected_format = (
|
||||
image_utils.get_image_format(
|
||||
path=base_path,
|
||||
# Snapshot operations requires 'qcow2'
|
||||
# backing file to be allowed
|
||||
allow_qcow2_backing_file=True))
|
||||
|
||||
if inspected_format == 'raw':
|
||||
# No qcow2 header found, assuming raw format
|
||||
admin_metadata['base_format'] = 'raw'
|
||||
admin_metadata['format'] = 'raw'
|
||||
msg = _("Recovered base_format as 'raw' for "
|
||||
"volume %(volume_id)s from format "
|
||||
"inspection.")
|
||||
LOG.info(
|
||||
msg, {'volume_id': snapshot.volume.id})
|
||||
update_metadata = True
|
||||
else:
|
||||
# Format inspection failed or non-raw format
|
||||
# detected (e.g. qcow2)
|
||||
msg = _("Unable to recover base_format "
|
||||
"for volume %(volume_id)s: can't "
|
||||
"determine original format from "
|
||||
"format inspection.")
|
||||
LOG.warning(
|
||||
msg, {'volume_id': snapshot.volume.id})
|
||||
|
||||
else:
|
||||
# Volume file no longer exists, this means the
|
||||
# volume has been merged into the active snapshot
|
||||
# during a previous online snapshot deletion and
|
||||
# qcow2 can be assumed as the new base format.
|
||||
admin_metadata['base_format'] = 'qcow2'
|
||||
admin_metadata['format'] = 'qcow2'
|
||||
update_metadata = True
|
||||
|
||||
if update_metadata:
|
||||
snapshot.volume.save()
|
||||
|
||||
else:
|
||||
# T0 | T1 | T2 | T3
|
||||
# base | snapshot_file | higher_file | highest_file
|
||||
@@ -1587,6 +1682,30 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
new_snap_path]
|
||||
self._execute(*command, run_as_root=self._execute_as_root)
|
||||
|
||||
def _get_snapshots_from_snap_info(self, snap_info: dict) -> List[str]:
|
||||
"""Get list of snapshot IDs from snap_info dictionary.
|
||||
|
||||
Returns snapshot IDs from the snap_info dictionary, excluding the
|
||||
'active' key.
|
||||
|
||||
:param snap_info: Snapshot info dictionary
|
||||
:returns: List of snapshot IDs
|
||||
"""
|
||||
return [k for k in snap_info.keys() if k != 'active']
|
||||
|
||||
def _get_volume_snapshots(self, volume: objects.Volume) -> List[str]:
|
||||
"""Get list of existing snapshots for a volume.
|
||||
|
||||
Reads the volume's snapshot info file and returns the list of
|
||||
snapshot IDs.
|
||||
|
||||
:param volume: Volume object
|
||||
:returns: List of snapshot IDs
|
||||
"""
|
||||
info_path = self._local_path_volume_info(volume)
|
||||
snap_info = self._read_info_file(info_path, empty_if_missing=True)
|
||||
return self._get_snapshots_from_snap_info(snap_info)
|
||||
|
||||
def _create_snapshot(self, snapshot: objects.Snapshot) -> None:
|
||||
"""Create a snapshot.
|
||||
|
||||
@@ -1715,23 +1834,19 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
snapshot.volume)
|
||||
new_snap_path = self._get_new_snap_path(snapshot)
|
||||
active = os.path.basename(new_snap_path)
|
||||
active_format = 'qcow2'
|
||||
|
||||
if self._is_volume_attached(snapshot.volume):
|
||||
self._create_snapshot_online(snapshot,
|
||||
backing_filename,
|
||||
new_snap_path)
|
||||
# Update the format for the volume and the connection_info. The
|
||||
# connection_info needs to reflect the current volume format in
|
||||
# The connection_info needs to reflect the current volume format in
|
||||
# order for Nova to create the disk device correctly whenever the
|
||||
# instance is stopped/started or rebooted.
|
||||
new_format = 'qcow2'
|
||||
snapshot.volume.admin_metadata['format'] = new_format
|
||||
with snapshot.volume.obj_as_admin():
|
||||
snapshot.volume.save()
|
||||
# Update reference in the only attachment (no multi-attach support)
|
||||
# instance is stopped/started or rebooted. Update reference in the
|
||||
# only attachment (no multi-attach support)
|
||||
attachment = snapshot.volume.volume_attachment[0]
|
||||
attachment.connection_info['name'] = active
|
||||
attachment.connection_info['format'] = new_format
|
||||
attachment.connection_info['format'] = active_format
|
||||
# Let OVO know it has been updated
|
||||
attachment.connection_info = attachment.connection_info
|
||||
attachment.save()
|
||||
@@ -1740,6 +1855,30 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
backing_filename,
|
||||
new_snap_path)
|
||||
|
||||
# Update volume format metadata to match active snapshot
|
||||
with snapshot.volume.obj_as_admin():
|
||||
admin_metadata = snapshot.volume.admin_metadata
|
||||
update_metadata = False
|
||||
volume_has_snapshots = (
|
||||
len(self._get_snapshots_from_snap_info(snap_info)) > 0)
|
||||
|
||||
if 'base_format' not in admin_metadata:
|
||||
if not volume_has_snapshots:
|
||||
# This is the first snapshot, we need to save the original
|
||||
# volume format so we can later restore it if all snapshots
|
||||
# are eventually deleted offline.
|
||||
admin_metadata['base_format'] = admin_metadata['format']
|
||||
update_metadata = True
|
||||
|
||||
# No need to update the format metadata if it already matches the
|
||||
# active snapshot.
|
||||
if admin_metadata['format'] != active_format:
|
||||
admin_metadata['format'] = active_format
|
||||
update_metadata = True
|
||||
|
||||
if update_metadata:
|
||||
snapshot.volume.save()
|
||||
|
||||
snap_info['active'] = active
|
||||
snap_info[snapshot.id] = active
|
||||
self._write_info_file(info_path, snap_info)
|
||||
@@ -1824,7 +1963,6 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
# active file never changes
|
||||
info_path = self._local_path_volume_info(snapshot.volume)
|
||||
snap_info = self._read_info_file(info_path)
|
||||
update_format = False
|
||||
|
||||
if utils.paths_normcase_equal(info['active_file'],
|
||||
info['snapshot_file']):
|
||||
@@ -1846,7 +1984,19 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
'volume_id': snapshot.volume.id}
|
||||
|
||||
del snap_info[snapshot.id]
|
||||
update_format = True
|
||||
|
||||
# If base_id is None, it means we are deleting the oldest snapshot
|
||||
if info['base_id'] is None:
|
||||
with snapshot.volume.obj_as_admin():
|
||||
admin_metadata = snapshot.volume.admin_metadata
|
||||
# When deleting the oldest snapshot, The previous base file
|
||||
# is merged into the active file, which then becomes the
|
||||
# new base. Consequently, the base_format must be updated
|
||||
# to match the active format.
|
||||
if 'format' in admin_metadata:
|
||||
admin_metadata['base_format'] = (
|
||||
admin_metadata['format'])
|
||||
snapshot.volume.save()
|
||||
else:
|
||||
# blockCommit snapshot into base
|
||||
# info['base'] <= snapshot_file
|
||||
@@ -1862,11 +2012,6 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
|
||||
self._nova_assisted_vol_snap_delete(context, snapshot, delete_info)
|
||||
|
||||
if update_format:
|
||||
snapshot.volume.admin_metadata['format'] = 'qcow2'
|
||||
with snapshot.volume.obj_as_admin():
|
||||
snapshot.volume.save()
|
||||
|
||||
# Write info file updated above
|
||||
self._write_info_file(info_path, snap_info)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user