diff --git a/.gitreview b/.gitreview index 99feb9c2668..9d9b34e1a19 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/cinder.git +defaultbranch=stable/2026.1 diff --git a/cinder/backup/drivers/ceph.py b/cinder/backup/drivers/ceph.py index 21e0664569c..ab7807ce074 100644 --- a/cinder/backup/drivers/ceph.py +++ b/cinder/backup/drivers/ceph.py @@ -458,6 +458,10 @@ def _transfer_data(self, return if (discard_zeros and volume_utils.is_all_zero(data)): + # Skip writing the zero chunk but still move the destination + # forward, otherwise the next chunk overwrites it and the + # restored volume ends up shifted (bug #2155612). + dest.seek(len(data), os.SEEK_CUR) action = "Discarded" else: dest.write(data) diff --git a/cinder/image/glance.py b/cinder/image/glance.py index 6ccfcbe05da..aaebdadaae7 100644 --- a/cinder/image/glance.py +++ b/cinder/image/glance.py @@ -643,9 +643,12 @@ def _translate_to_glance(image_meta: dict[str, Any]) -> dict[str, Any]: # NOTE(tsekiyama): From the Image API v2, custom properties must # be stored in image_meta directly, instead of the 'properties' key. - properties = image_meta.get('properties') - if properties: - image_meta.update(properties) + # NOTE(al3jandro) ensure key deletion even when properties={} + # (LP#2144550) + if 'properties' in image_meta: + properties = image_meta['properties'] + if properties: + image_meta.update(properties) del image_meta['properties'] return image_meta @@ -670,7 +673,7 @@ def _is_image_available(self, getattr(image, 'visibility', 'private') == 'public'): return True - properties = image.properties + properties = getattr(image, 'properties', {}) if context.project_id and ('owner_id' in properties): return str(properties['owner_id']) == str(context.project_id) diff --git a/cinder/tests/unit/backup/drivers/test_backup_ceph.py b/cinder/tests/unit/backup/drivers/test_backup_ceph.py index a16042b751b..347a488f0a3 100644 --- a/cinder/tests/unit/backup/drivers/test_backup_ceph.py +++ b/cinder/tests/unit/backup/drivers/test_backup_ceph.py @@ -425,6 +425,39 @@ def test_transfer_data_from_file_to_file(self): # Ensure the files are equal self.assertEqual(checksum.digest(), self.checksum.digest()) + @common_mocks + def test_transfer_data_discard_zeros_advances_offset(self): + # bug #2155612: a discarded zero chunk must still advance the + # destination offset, else later chunks land at the wrong place and + # a sparse volume restored to a new volume comes back shifted. + chunk = self.chunk_size + self.service.chunk_size = chunk + src_data = (b'\xab' * chunk + b'\x00' * chunk + b'\xcd' * chunk + + b'\x00' * chunk + b'\xef' * chunk) + dest_data = bytearray(len(src_data)) + + def fake_read(offset, length): + return src_data[offset:offset + length] + + def fake_write(data, offset): + dest_data[offset:offset + len(data)] = data + + src_rbd = mock.Mock() + src_rbd.read.side_effect = fake_read + src_rbd.size.return_value = len(src_data) + dest_rbd = mock.Mock() + dest_rbd.write.side_effect = fake_write + dest_rbd.size.return_value = len(src_data) + + src_io = self._get_wrapped_rbd_io(src_rbd) + dest_io = self._get_wrapped_rbd_io(dest_rbd) + + with mock.patch.object(ceph.time, 'time', self.time_inc): + self.service._transfer_data(src_io, 'src_foo', dest_io, 'dest_foo', + len(src_data), discard_zeros=True) + + self.assertEqual(src_data, bytes(dest_data)) + @common_mocks def test_backup_volume_from_file(self): checksum = hashlib.sha256() diff --git a/cinder/tests/unit/group/test_groups_manager.py b/cinder/tests/unit/group/test_groups_manager.py index d4d6f07a95c..2f1024c1b8c 100644 --- a/cinder/tests/unit/group/test_groups_manager.py +++ b/cinder/tests/unit/group/test_groups_manager.py @@ -504,21 +504,24 @@ def test_create_group_from_src(self, self.notifier.notifications) self.volume.delete_group(self.context, group2) - - if len(self.notifier.notifications) > 9: - self.assertFalse(self.notifier.notifications[10], + if len(self.notifier.notifications) > 10: + self.assertFalse(self.notifier.notifications[11], self.notifier.notifications) - self.assertEqual(9, len(self.notifier.notifications), + self.assertEqual(10, len(self.notifier.notifications), self.notifier.notifications) msg = self.notifier.notifications[6] self.assertEqual('group.delete.start', msg['event_type']) expected['status'] = fields.GroupStatus.AVAILABLE self.assertDictEqual(expected, msg['payload']) + msg = self.notifier.notifications[7] + self.assertEqual('volume.delete.start', msg['event_type']) msg = self.notifier.notifications[8] self.assertEqual('group.delete.end', msg['event_type']) expected['status'] = fields.GroupStatus.DELETED self.assertDictEqual(expected, msg['payload']) + msg = self.notifier.notifications[9] + self.assertEqual('volume.delete.end', msg['event_type']) grp2 = objects.Group.get_by_id( context.get_admin_context(read_deleted='yes'), group2.id) diff --git a/cinder/tests/unit/image/test_glance.py b/cinder/tests/unit/image/test_glance.py index 2f4385476cf..b61261ecc6f 100644 --- a/cinder/tests/unit/image/test_glance.py +++ b/cinder/tests/unit/image/test_glance.py @@ -479,7 +479,7 @@ def test_detail_marker(self): 'status': None, 'protected': None, 'name': 'TestImage %d' % (i), - 'properties': {'properties': {}}, + 'properties': {}, 'size': None, 'min_disk': None, 'min_ram': None, @@ -536,7 +536,7 @@ def test_detail_marker_and_limit(self): 'status': None, 'protected': None, 'name': 'TestImage %d' % (i), - 'properties': {'properties': {}}, + 'properties': {}, 'size': None, 'min_disk': None, 'min_ram': None, @@ -671,7 +671,7 @@ def test_show_passes_through_to_client(self): 'updated_at': self.NOW_DATETIME, 'deleted': None, 'status': None, - 'properties': {'is_public': True, 'properties': {}}, + 'properties': {'is_public': True}, 'owner': None, 'visibility': None } @@ -698,7 +698,7 @@ def test_show_passes_when_is_admin_in_the_context(self): 'updated_at': self.NOW_DATETIME, 'deleted': None, 'status': None, - 'properties': {'properties': {}}, + 'properties': {}, 'owner': None, 'visibility': None } @@ -726,7 +726,7 @@ def test_show_passes_when_is_public_in_visibility_param(self): 'updated_at': self.NOW_DATETIME, 'deleted': None, 'status': None, - 'properties': {'properties': {}}, + 'properties': {}, 'owner': None, 'visibility': 'public' } @@ -786,7 +786,7 @@ def test_detail_passes_through_to_client(self): 'updated_at': self.NOW_DATETIME, 'deleted': None, 'status': None, - 'properties': {'is_public': True, 'properties': {}}, + 'properties': {'is_public': True}, 'owner': None, 'visibility': None }, diff --git a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py index 379a96ea6fc..a17a8f5b6d1 100644 --- a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py +++ b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py @@ -7163,6 +7163,7 @@ def test_driver_login_with_wrong_credential_and_replication_enabled(self): mock_client.assert_has_calls(expected) self.assertTrue(common._replication_enabled) + @test.testtools.skip("launchpad bug #2146339") def test_thread_local_sessions_are_isolated(self): self.setup_driver() session_counter = [0] diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py index df14991ea75..29d163506c0 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py @@ -130,6 +130,7 @@ def test_create_volume(self): self.mock_object(self.library, '_add_lun_to_table') self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') self.mock_object(self.library, '_get_volume_model_update') + self.zapi_client.get_ontap_version.return_value = (9, 14, 0) self.library.create_volume(fake.VOLUME) @@ -159,6 +160,7 @@ def test_create_volume_asar2(self): self.mock_object(self.library, '_add_lun_to_table') self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') self.mock_object(self.library, '_get_volume_model_update') + self.zapi_client.get_ontap_version.return_value = (9, 14, 0) self.library.create_volume(fake.VOLUME) @@ -234,6 +236,56 @@ def test_create_volume_space_allocation_extra_spec_true(self): 0, self.library._mark_qos_policy_group_for_deletion.call_count) self.assertEqual(0, block_base.LOG.error.call_count) + def test_create_volume_space_allocation_ontap_pre_9_15_1(self): + """No extra spec + ONTAP < 9.15.1 → space_allocation disabled.""" + volume_size_in_bytes = int(fake.SIZE) * units.Gi + self.mock_object(na_utils, 'get_volume_extra_specs', + return_value={}) + self.mock_object(na_utils, 'log_extra_spec_warnings') + self.mock_object(block_base, 'LOG') + self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) + self.mock_object(self.library, '_setup_qos_for_volume', + return_value=fake.QOS_POLICY_GROUP_INFO) + self.mock_object(self.library, '_create_lun') + self.mock_object(self.library, '_create_lun_handle') + self.mock_object(self.library, '_add_lun_to_table') + self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') + self.mock_object(self.library, '_get_volume_model_update') + self.zapi_client.get_ontap_version.return_value = (9, 14, 0) + + self.library.create_volume(fake.VOLUME) + + self.library._create_lun.assert_called_once_with( + fake.POOL_NAME, fake.LUN_NAME, volume_size_in_bytes, + fake.LUN_METADATA, + fake.QOS_POLICY_GROUP_NAME, False) + + def test_create_volume_space_allocation_ontap_9_15_1(self): + """No extra spec + ONTAP >= 9.15.1 → space_allocation enabled.""" + volume_size_in_bytes = int(fake.SIZE) * units.Gi + self.mock_object(na_utils, 'get_volume_extra_specs', + return_value={}) + self.mock_object(na_utils, 'log_extra_spec_warnings') + self.mock_object(block_base, 'LOG') + self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) + self.mock_object(self.library, '_setup_qos_for_volume', + return_value=fake.QOS_POLICY_GROUP_INFO) + self.mock_object(self.library, '_create_lun') + self.mock_object(self.library, '_create_lun_handle') + self.mock_object(self.library, '_add_lun_to_table') + self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') + self.mock_object(self.library, '_get_volume_model_update') + self.zapi_client.get_ontap_version.return_value = (9, 15, 1) + + self.library.create_volume(fake.VOLUME) + + self.library._create_lun.assert_called_once_with( + fake.POOL_NAME, fake.LUN_NAME, volume_size_in_bytes, + fake.LUN_METADATA_WITH_SPACE_ALLOCATION, + fake.QOS_POLICY_GROUP_NAME, False) + def test_create_volume_no_pool(self): self.mock_object(volume_utils, 'extract_host', return_value=None) @@ -259,6 +311,7 @@ def test_create_volume_exception_path(self): return_value=fake.QOS_POLICY_GROUP_INFO) self.mock_object(self.library, '_create_lun', side_effect=Exception) self.mock_object(self.library, '_mark_qos_policy_group_for_deletion') + self.zapi_client.get_ontap_version.return_value = (9, 14, 0) self.assertRaises(exception.VolumeBackendAPIException, self.library.create_volume, fake.VOLUME) diff --git a/cinder/tests/unit/volume/drivers/test_pure.py b/cinder/tests/unit/volume/drivers/test_pure.py index 4e1867a54a7..a008e2977bb 100644 --- a/cinder/tests/unit/volume/drivers/test_pure.py +++ b/cinder/tests/unit/volume/drivers/test_pure.py @@ -5003,6 +5003,58 @@ def test_retype_qos_reset_iops(self, mock_fa): self.assertTrue(did_retype) self.assertIsNone(model_update) + @ddt.data( + # maxIOPS and maxBWS both set -> use both, untouched. + {"qos": {"maxIOPS": 100, "maxBWS": 1048576, + "maxIOPS_per_GB": 0, "maxBWS_per_GB": 0}, + "exp_iops": 100, "exp_bws": 1048576}, + # maxIOPS unset -> falls back to MAX_IOPS. + {"qos": {"maxIOPS": 0, "maxBWS": 1048576, + "maxIOPS_per_GB": 0, "maxBWS_per_GB": 0}, + "exp_iops": MAX_IOPS, "exp_bws": 1048576}, + # maxBWS unset -> falls back to MAX_BWS. + {"qos": {"maxIOPS": 100, "maxBWS": 0, + "maxIOPS_per_GB": 0, "maxBWS_per_GB": 0}, + "exp_iops": 100, "exp_bws": MAX_BWS}, + # neither set -> both fall back to the maximums. + {"qos": {"maxIOPS": 0, "maxBWS": 0, + "maxIOPS_per_GB": 0, "maxBWS_per_GB": 0}, + "exp_iops": MAX_IOPS, "exp_bws": MAX_BWS}, + ) + # Patch the individual models so each gets a fresh, isolated mock. The + # ``flasharray`` module is a process-wide Mock (see the module-level + # ``sys.modules['pypureclient']`` stub), so relying on it directly and + # resetting it counts calls from other tests in the same worker. + @mock.patch(DRIVER_PATH + ".flasharray.VolumePatch") + @mock.patch(DRIVER_PATH + ".flasharray.QosPatch") + @mock.patch(DRIVER_PATH + ".flasharray.QosBandwidthLimitPatch") + @mock.patch(DRIVER_PATH + ".flasharray.QosIopsLimitPatch") + @mock.patch(DRIVER_PATH + ".flasharray.Qos") + def test_set_qos_uses_qos_patch_models(self, data, mock_qos, + mock_iops_patch, mock_bws_patch, + mock_qos_patch, mock_vol_patch): + qos = data["qos"] + exp_iops = data["exp_iops"] + exp_bws = data["exp_bws"] + _, vol_name = self.new_fake_vol() + + self.driver.set_qos(self.array, vol_name, 1, qos) + + # The wrapped limit models must be used with the expected values... + mock_iops_patch.assert_called_once_with(exp_iops) + mock_bws_patch.assert_called_once_with(exp_bws) + # ...wrapped inside a QosPatch (not a flat Qos)... + mock_qos_patch.assert_called_once_with( + iops_limit=mock_iops_patch.return_value, + bandwidth_limit=mock_bws_patch.return_value) + mock_qos.assert_not_called() + # ...and applied to the volume via a VolumePatch. + mock_vol_patch.assert_called_once_with( + qos=mock_qos_patch.return_value) + self.array.patch_volumes.assert_called_once_with( + names=[vol_name], + volume=mock_vol_patch.return_value) + class PureISCSIDriverTestCase(PureBaseSharedDriverTestCase): diff --git a/cinder/tests/unit/volume/test_image.py b/cinder/tests/unit/volume/test_image.py index 72c16799a99..4a81cf3b33e 100644 --- a/cinder/tests/unit/volume/test_image.py +++ b/cinder/tests/unit/volume/test_image.py @@ -25,6 +25,7 @@ from cinder import db from cinder import exception +from cinder.image import image_utils from cinder.message import message_field from cinder import objects from cinder.objects import fields @@ -852,3 +853,28 @@ def test_merge_volume_image_meta(self, mock_get_img_meta): # correct key_id self.assertEqual(image_meta['cinder_encryption_key_id'], sent_to_glance['cinder_encryption_key_id']) + + @mock.patch('cinder.volume.api.API.get_volume_image_metadata', + return_value={'image_id': 'base-image-id', + 'signature_verified': 'False'}) + def test_filter_reserved_namespaces_metadata(self, mock_get_img_meta): + # testing the fix for LP#2144550 + image_meta = { + 'container_format': 'bare', + 'disk_format': 'raw', + } + + volume_api = cinder.volume.api.API() + volume_api._merge_volume_image_meta(None, None, image_meta) + + self.assertIn('properties', image_meta) + self.assertIn('signature_verified', image_meta.get('properties')) + + filtered = image_utils.filter_out_reserved_namespaces_metadata( + image_meta) + self.assertEqual({}, filtered.get('properties')) + + translate = cinder.image.glance.GlanceImageService._translate_to_glance + sent_to_glance = translate(filtered) + + self.assertNotIn('properties', sent_to_glance) diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index 32213a299c1..7e1a855a20b 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -238,9 +238,19 @@ def create_volume(self, volume): extra_specs = na_utils.get_volume_extra_specs(volume) - space_allocation = volume_utils.is_boolean_str( - extra_specs.get('netapp:space_allocation') - ) + ontap_version = self.zapi_client.get_ontap_version(cached=True) + space_allocation_spec = extra_specs.get('netapp:space_allocation') + if space_allocation_spec is not None: + space_allocation = volume_utils.is_boolean_str( + space_allocation_spec + ) + elif ontap_version >= (9, 15, 1): + # ONTAP 9.15.1+ enables space-allocation by default for new LUNs + # Ref: https://docs.netapp.com/us-en/ontap/release-notes/ + # defaults-limits.html + space_allocation = True + else: + space_allocation = False LOG.debug('create_volume space_allocation %r', space_allocation) lun_name = volume['name'] diff --git a/cinder/volume/drivers/pure.py b/cinder/volume/drivers/pure.py index c8c38e56c49..83c2f8b3efd 100644 --- a/cinder/volume/drivers/pure.py +++ b/cinder/volume/drivers/pure.py @@ -199,6 +199,22 @@ TAG_NAMESPACE = "openstack-integration.purestorage.com" +def _build_qos_patch(iops_limit, bandwidth_limit): + """Build the QoS object for a VolumePatch/VolumeGroupPatch request. + + FlashArray REST API 2.52 (py-pure-client>=1.86.0) changed the ``*Patch`` + models so that ``VolumePatch.qos`` and ``VolumeGroupPatch.qos`` take a + ``QosPatch`` whose ``iops_limit``/``bandwidth_limit`` are wrapped objects + (``QosIopsLimitPatch``/``QosBandwidthLimitPatch``) rather than a flat + ``Qos`` with integer limits. The ``*Post`` models (VolumePost/ + VolumeGroupPost) still take a flat ``Qos``, so those call sites are left + unchanged. + """ + return flasharray.QosPatch( + iops_limit=flasharray.QosIopsLimitPatch(iops_limit), + bandwidth_limit=flasharray.QosBandwidthLimitPatch(bandwidth_limit)) + + class PureDriverException(exception.VolumeDriverException): message = _("Pure Storage Cinder driver failure: %(reason)s") @@ -382,25 +398,25 @@ def set_qos(self, array, vol_name, vol_size, qos): if qos['maxIOPS'] == 0 and qos['maxBWS'] == 0: array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=MAX_IOPS, bandwidth_limit=MAX_BWS))) elif qos['maxIOPS'] == 0: array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=MAX_IOPS, bandwidth_limit=qos['maxBWS']))) elif qos['maxBWS'] == 0: array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=qos['maxIOPS'], bandwidth_limit=MAX_BWS))) else: array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=qos['maxIOPS'], bandwidth_limit=qos['maxBWS']))) return @@ -885,7 +901,7 @@ def create_volume_from_snapshot(self, volume, snapshot, cgsnapshot=False): else: current_array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=MAX_IOPS, bandwidth_limit=MAX_BWS))) @@ -2153,8 +2169,8 @@ def manage_existing(self, volume, existing_ref): current_array.patch_volumes( names=[new_vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos(iops_limit=MAX_IOPS, - bandwidth_limit=MAX_BWS))) + qos=_build_qos_patch(iops_limit=MAX_IOPS, + bandwidth_limit=MAX_BWS))) # If we are managing to a volume type that is a volume group # make sure that the target volume group exists with the # correct QoS settings. @@ -3136,7 +3152,7 @@ def retype(self, context, volume, new_type, diff, host): else: current_array.patch_volumes(names=[vol_name], volume=flasharray.VolumePatch( - qos=flasharray.Qos( + qos=_build_qos_patch( iops_limit=MAX_IOPS, bandwidth_limit=MAX_BWS))) @@ -3625,7 +3641,7 @@ def _create_volume_group_if_not_exist(self, res = source_array.patch_volume_groups( names=[vgname], volume_group=flasharray.VolumeGroupPatch( - qos=flasharray.Qos( + qos=_build_qos_patch( bandwidth_limit=vg_bws, iops_limit=vg_iops))) if res.status_code == 400: diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index aa9cfa806c4..84cd98a3beb 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -3803,7 +3803,7 @@ def delete_group(self, self._check_is_our_resource(vol_obj) self._notify_about_group_usage( - context, group, "delete.start") + context, group, "delete.start", volumes=volumes) volumes_model_update = None model_update = None @@ -3906,7 +3906,7 @@ def delete_group(self, group.destroy() self._notify_about_group_usage( - context, group, "delete.end") + context, group, "delete.end", volumes=volumes) self.publish_service_capabilities(context) LOG.info("Delete group " "completed successfully.", diff --git a/pyproject.toml b/pyproject.toml index 4eb0f976efb..8ce5d6d0cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ all = [ "python-3parclient>=4.2.10", # Apache-2.0 "krest>=1.3.0", # Apache-2.0 "infinisdk>=103.0.1", # BSD-3 - "py-pure-client>=1.47.0", # BSD + "py-pure-client>=1.89.0", # BSD "rsd-lib>=1.1.0", # Apache-2.0 "storpool>=7.1.0", # Apache-2.0 "storpool.spopenstack>=2.2.1", # Apache-2.0 @@ -122,7 +122,7 @@ infinidat = [ "infinisdk>=103.0.1", # BSD-3 ] pure = [ - "py-pure-client>=1.47.0", # BSD + "py-pure-client>=1.89.0", # BSD ] rsd = [ "rsd-lib>=1.1.0", # Apache-2.0 diff --git a/releasenotes/notes/bug-2139068-b65849ce5771cb1b.yaml b/releasenotes/notes/bug-2139068-b65849ce5771cb1b.yaml new file mode 100644 index 00000000000..0576944476b --- /dev/null +++ b/releasenotes/notes/bug-2139068-b65849ce5771cb1b.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + `Bug #2139068 `_: Fixed + an issue where ``volume.delete.end`` notifications were not sent when + volumes were deleted as part of a group deletion process. This ensures + delete notifications are properly emitted for every volume removed during + group deletion. diff --git a/releasenotes/notes/bug_2144550-aa8e6e085f6507aa.yaml b/releasenotes/notes/bug_2144550-aa8e6e085f6507aa.yaml new file mode 100644 index 00000000000..a3fa6b48a76 --- /dev/null +++ b/releasenotes/notes/bug_2144550-aa8e6e085f6507aa.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + `Bug #2144550 `_: Fixed + a bug that could cause image creation from volumes to fail due to + incorrect handling of the volume metadata passed to Glance. diff --git a/releasenotes/notes/fix-ceph-backup-sparse-restore-corruption-2155612a1b2c3d4e5f.yaml b/releasenotes/notes/fix-ceph-backup-sparse-restore-corruption-2155612a1b2c3d4e5f.yaml new file mode 100644 index 00000000000..c22851d59d2 --- /dev/null +++ b/releasenotes/notes/fix-ceph-backup-sparse-restore-corruption-2155612a1b2c3d4e5f.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Ceph backup driver `bug #2155612 + `_: Fixed data corruption + when restoring a sparse volume (such as a LUKS-encrypted one) to a new + volume. The driver skipped all-zero chunks without advancing the + destination offset, so every subsequent non-zero chunk was written at the + wrong location and the restored volume was silently truncated even though + the restore reported success. The destination offset is now advanced when + a zero chunk is discarded, so the restored layout matches the source. diff --git a/releasenotes/notes/netapp-lun-space-allocation-ontap-9151-a3f7c2d18b4e5091.yaml b/releasenotes/notes/netapp-lun-space-allocation-ontap-9151-a3f7c2d18b4e5091.yaml new file mode 100644 index 00000000000..219c6daaac6 --- /dev/null +++ b/releasenotes/notes/netapp-lun-space-allocation-ontap-9151-a3f7c2d18b4e5091.yaml @@ -0,0 +1,22 @@ +--- +fixes: + - | + NetApp ONTAP block driver + `bug #2152031 `_: + Fixed an inconsistency in the NetApp ONTAP block driver where LUN + space-allocation (thin-provisioning SCSI UNMAP support) was always + disabled when creating volumes, regardless of the ONTAP version in + use. + + Starting with ONTAP 9.15.1, NetApp changed the default LUN + space-allocation setting from disabled to enabled. The driver now + reflects this: when the ``netapp:space_allocation`` extra spec is + not set, the driver checks the ONTAP version and enables + space-allocation by default for ONTAP 9.15.1 and later, preserving + the previous disabled default for older versions. + + The explicit ``netapp:space_allocation`` extra spec continues to + take precedence over version-based defaults. + + `ONTAP defaults and limits + `_ \ No newline at end of file diff --git a/releasenotes/notes/pure-fix-qos-patch-models-9b1c0c1f0a3d4e5b.yaml b/releasenotes/notes/pure-fix-qos-patch-models-9b1c0c1f0a3d4e5b.yaml new file mode 100644 index 00000000000..e58207abe87 --- /dev/null +++ b/releasenotes/notes/pure-fix-qos-patch-models-9b1c0c1f0a3d4e5b.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Everpure driver `bug #2158313 + `_: Fixed + creating a volume from a snapshot, and + setting QoS limits, with newer ``py-pure-client`` releases. +upgrade: + - | + The Everpure driver now requires ``py-pure-client>=1.89.0``. diff --git a/requirements.txt b/requirements.txt index 7fae990da4b..7f9d5bdf3f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ oslo.upgradecheck>=1.1.1 # Apache-2.0 oslo.utils>=6.0.0 # Apache-2.0 oslo.versionedobjects>=2.4.0 # Apache-2.0 osprofiler>=3.4.0 # Apache-2.0 -packaging>=20.4 +packaging>=20.9 # Apache-2.0 paramiko>=2.7.2 # LGPLv2.1+ Paste>=3.4.3 # MIT PasteDeploy>=2.1.0 # MIT @@ -60,4 +60,3 @@ boto3>=1.18.49 # Apache-2.0 distro>=1.8.0 # Apache-2.0 tzdata>=2022.4 # MIT cachetools>=4.2.1 # MIT -packaging>=20.9 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 5dabb7b3c31..0190cb729f9 100644 --- a/tox.ini +++ b/tox.ini @@ -55,7 +55,7 @@ passenv = # With constraints in the install_command tox will always honor our # constraints. install_command = - python -m pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} {opts} {packages} + python -m pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2026.1} {opts} {packages} [testenv:functional] install_command = {[testenv:py3]install_command} @@ -150,7 +150,7 @@ commands = {posargs} # we intentionally put the constraints in the install_command, not the # deps ... see comment near the top of this file install_command = - python -m pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} {opts} {packages} + python -m pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2026.1} {opts} {packages} allowlist_externals = rm deps = doc8