From 3508d01a7ef3af91f9a55c78ca9a8089dd111835 Mon Sep 17 00:00:00 2001 From: michael-mcaleer Date: Fri, 26 Jul 2019 16:27:29 +0000 Subject: [PATCH] PowerMax Driver - Volume & Snapshot Metadata All volumes and snapshots created using the PowerMax for Cinder driver now have additional metadata included pertaining to the details of the asset on the backend storage array. Change-Id: I35b8685e127605d19f2316802d44b7373a673e98 --- .../dell_emc/powermax/powermax_data.py | 55 +++++ .../dell_emc/powermax/test_powermax_common.py | 218 +++++++++++++++--- .../powermax/test_powermax_replication.py | 42 +++- .../dell_emc/powermax/test_powermax_utils.py | 20 ++ .../drivers/dell_emc/powermax/common.py | 151 +++++++++++- cinder/volume/drivers/dell_emc/powermax/fc.py | 1 + .../volume/drivers/dell_emc/powermax/iscsi.py | 1 + .../volume/drivers/dell_emc/powermax/utils.py | 5 +- ...owermax-vol-metadata-acd2555818d25b72.yaml | 6 + 9 files changed, 452 insertions(+), 47 deletions(-) create mode 100644 releasenotes/notes/powermax-vol-metadata-acd2555818d25b72.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py index 200493bbdd0..81978438443 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/powermax_data.py @@ -35,6 +35,7 @@ class PowerMaxData(object): array = '000197800123' uni_array = u'000197800123' array_herc = '000197900123' + array_model = 'PowerMax_8000' srp = 'SRP_1' srp2 = 'SRP_2' slo = 'Diamond' @@ -78,6 +79,7 @@ class PowerMaxData(object): no_slo_sg_name = 'OS-HostX-No_SLO-OS-fibre-PG' temp_snapvx = 'temp-00001-snapshot_for_clone' next_gen_ucode = 5978 + gvg_group_id = 'test-gvg' # connector info wwpn1 = '123456789012345' @@ -1124,3 +1126,56 @@ class PowerMaxData(object): {'generation': 1, 'expired': False, 'copy_mode': False, 'snap_name': 'temp-000AA-snapshot_for_clone', 'state': 'Copied', 'source_vol_id': device_id, 'target_vol_id': device_id4}] + + device_label = 'OS-00001' + priv_vol_response_rep = { + 'volumeHeader': { + 'private': False, 'capGB': 1.0, 'capMB': 1026.0, + 'serviceState': 'Normal', 'emulationType': 'FBA', + 'volumeId': '00001', 'status': 'Ready', 'mapped': False, + 'numStorageGroups': 0, 'reservationInfo': {'reserved': False}, + 'encapsulated': False, 'formattedName': '00001', + 'system_resource': False, 'numSymDevMaskingViews': 0, + 'nameModifier': "", 'configuration': 'TDEV', + 'userDefinedIdentifier': 'OS-00001'}, + 'maskingInfo': {'masked': False}, + 'rdfInfo': { + 'dynamicRDF': False, 'RDF': True, + 'concurrentRDF': False, + 'getDynamicRDFCapability': 'RDF1_Capable', 'RDFA': False, + 'RDFSession': [ + {'SRDFStatus': 'Ready', + 'SRDFReplicationMode': 'Synchronized', + 'remoteDeviceID': device_id2, + 'remoteSymmetrixID': remote_array, + 'SRDFGroupNumber': 1, + 'SRDFRemoteGroupNumber': 1}]}} + + priv_vol_response_no_rep = { + 'volumeHeader': { + 'private': False, 'capGB': 1.0, 'capMB': 1026.0, + 'serviceState': 'Normal', 'emulationType': 'FBA', + 'volumeId': '00001', 'status': 'Ready', 'mapped': False, + 'numStorageGroups': 0, 'reservationInfo': {'reserved': False}, + 'encapsulated': False, 'formattedName': '00001', + 'system_resource': False, 'numSymDevMaskingViews': 0, + 'nameModifier': "", 'configuration': 'TDEV', + 'userDefinedIdentifier': 'OS-00001'}, + 'maskingInfo': {'masked': False}, + 'rdfInfo': {'RDF': False}} + + snap_device_label = ('%(dev)s:%(label)s' % {'dev': device_id, + 'label': managed_snap_id}) + priv_snap_response = { + 'deviceName': snap_device_label, 'snapshotLnks': [], + 'snapshotSrcs': [ + {'generation': 0, + 'linkedDevices': [ + {'targetDevice': device_id2, 'percentageCopied': 100, + 'state': 'Copied', 'copy': True, 'defined': True, + 'linked': True}], + 'snapshotName': test_snapshot_snap_name, + 'state': 'Established'}]} + + volume_metadata = { + 'DeviceID': device_id, 'ArrayID': array, 'ArrayModel': array_model} diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py index a7fc2c2028e..241af93427f 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_common.py @@ -136,15 +136,29 @@ class PowerMaxCommonTest(test.TestCase): exception.VolumeBackendAPIException, self.common._get_slo_workload_combinations, array_info) - def test_create_volume(self): + @mock.patch.object( + common.PowerMaxCommon, 'get_volume_metadata', + return_value={'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'}) + def test_create_volume(self, mck_meta): ref_model_update = ( - {'provider_location': six.text_type(self.data.provider_location)}) - model_update = self.common.create_volume(self.data.test_volume) + {'provider_location': six.text_type(self.data.provider_location), + 'metadata': {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}}) + volume = deepcopy(self.data.test_volume) + volume.metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} + model_update = self.common.create_volume(volume) self.assertEqual(ref_model_update, model_update) - def test_create_volume_qos(self): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_volume_qos(self, mck_meta): ref_model_update = ( - {'provider_location': six.text_type(self.data.provider_location)}) + {'provider_location': six.text_type(self.data.provider_location), + 'metadata': ''}) extra_specs = deepcopy(self.data.extra_specs_intervals_set) extra_specs['qos'] = { 'total_iops_sec': '4000', 'DistributionType': 'Always'} @@ -154,7 +168,9 @@ class PowerMaxCommonTest(test.TestCase): self.assertEqual(ref_model_update, model_update) @mock.patch.object(common.PowerMaxCommon, '_clone_check') - def test_create_volume_from_snapshot(self, mck_clone_chk): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_volume_from_snapshot(self, mck_meta, mck_clone_chk): ref_model_update = ({'provider_location': six.text_type( deepcopy(self.data.provider_location_snapshot))}) model_update = self.common.create_volume_from_snapshot( @@ -174,7 +190,9 @@ class PowerMaxCommonTest(test.TestCase): ast.literal_eval(model_update['provider_location'])) @mock.patch.object(common.PowerMaxCommon, '_clone_check') - def test_cloned_volume(self, mck_clone_chk): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_cloned_volume(self, mck_meta, mck_clone_chk): ref_model_update = ({'provider_location': six.text_type( self.data.provider_location_clone)}) model_update = self.common.create_cloned_volume( @@ -189,11 +207,22 @@ class PowerMaxCommonTest(test.TestCase): mock_delete.assert_called_once_with(self.data.test_volume) @mock.patch.object(common.PowerMaxCommon, '_clone_check') - def test_create_snapshot(self, mck_clone_chk): - ref_model_update = ({'provider_location': six.text_type( - self.data.snap_location)}) + @mock.patch.object( + common.PowerMaxCommon, 'get_snapshot_metadata', + return_value={'snap-meta-key-1': 'snap-meta-value-1', + 'snap-meta-key-2': 'snap-meta-value-2'}) + def test_create_snapshot(self, mck_meta, mck_clone_chk): + ref_model_update = ( + {'provider_location': six.text_type(self.data.snap_location), + 'metadata': {'snap-meta-key-1': 'snap-meta-value-1', + 'snap-meta-key-2': 'snap-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}}) + snapshot = deepcopy(self.data.test_snapshot_manage) + snapshot.metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} model_update = self.common.create_snapshot( - self.data.test_snapshot, self.data.test_volume) + snapshot, self.data.test_volume) self.assertEqual(ref_model_update, model_update) def test_delete_snapshot(self): @@ -1261,15 +1290,25 @@ class PowerMaxCommonTest(test.TestCase): array, target_device_id, clone_name, extra_specs) - def test_manage_existing_success(self): + @mock.patch.object( + common.PowerMaxCommon, 'get_volume_metadata', + return_value={'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'}) + def test_manage_existing_success(self, mck_meta): external_ref = {u'source-name': u'00002'} provider_location = {'device_id': u'00002', 'array': u'000197800123'} - ref_update = {'provider_location': six.text_type(provider_location)} + ref_update = {'provider_location': six.text_type(provider_location), + 'metadata': {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}} + volume = deepcopy(self.data.test_volume) + volume.metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} with mock.patch.object( self.common, '_check_lun_valid_for_cinder_management', return_value=('vol1', 'test_sg')): - model_update = self.common.manage_existing( - self.data.test_volume, external_ref) + model_update = self.common.manage_existing(volume, external_ref) self.assertEqual(ref_update, model_update) @mock.patch.object( @@ -1604,7 +1643,9 @@ class PowerMaxCommonTest(test.TestCase): self.data.workload, volume_name, new_type, extra_specs) @mock.patch.object(masking.PowerMaxMasking, 'remove_and_reset_members') - def test_migrate_volume_success(self, mock_remove): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_migrate_volume_success(self, mck_meta, mock_remove): with mock.patch.object(self.rest, 'is_volume_in_storagegroup', return_value=True): device_id = self.data.device_id @@ -1645,8 +1686,10 @@ class PowerMaxCommonTest(test.TestCase): return_value=('Status', 'Data', 'Info')) @mock.patch.object(common.PowerMaxCommon, '_retype_remote_volume', return_value=True) - def test_migrate_in_use_volume(self, mck_remote_retype, mck_setup, - mck_retype, mck_cleanup): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_migrate_in_use_volume(self, mck_meta, mck_remote_retype, + mck_setup, mck_retype, mck_cleanup): # Array/Volume info array = self.data.array srp = self.data.srp @@ -1746,9 +1789,11 @@ class PowerMaxCommonTest(test.TestCase): return_value=('Status', 'Data', 'Info')) @mock.patch.object(common.PowerMaxCommon, '_retype_remote_volume', return_value=True) - def test_migrate_volume_attachment_path(self, mck_remote_retype, mck_setup, - mck_inuse_retype, mck_cleanup, - mck_retype): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_migrate_volume_attachment_path( + self, mck_meta, mck_remote_retype, mck_setup, mck_inuse_retype, + mck_cleanup, mck_retype): # Array/Volume info array = self.data.array srp = self.data.srp @@ -2109,7 +2154,9 @@ class PowerMaxCommonTest(test.TestCase): return_value=True) @mock.patch.object(volume_utils, 'is_group_a_type', return_value=False) - def test_create_group_from_src_success(self, mock_type, + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_group_from_src_success(self, mck_meta, mock_type, mock_cg_type, mock_info): ref_model_update = {'status': fields.GroupStatus.AVAILABLE} model_update, volumes_model_update = ( @@ -2233,17 +2280,26 @@ class PowerMaxCommonTest(test.TestCase): @mock.patch.object(rest.PowerMaxRest, 'get_volume_snap', return_value={'snap_name': 'snap_name'}) - def test_manage_snapshot_success(self, mock_snap): - snapshot = self.data.test_snapshot_manage + @mock.patch.object( + common.PowerMaxCommon, 'get_snapshot_metadata', + return_value={'snap-meta-key-1': 'snap-meta-value-1', + 'snap-meta-key-2': 'snap-meta-value-2'}) + def test_manage_snapshot_success(self, mck_meta, mock_snap): + snapshot = deepcopy(self.data.test_snapshot_manage) + snapshot.metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} existing_ref = {u'source-name': u'test_snap'} updates_response = self.common.manage_existing_snapshot( snapshot, existing_ref) prov_loc = {'source_id': self.data.device_id, 'snap_name': 'OS-%s' % existing_ref['source-name']} - updates = {'display_name': 'my_snap', - 'provider_location': six.text_type(prov_loc)} + 'provider_location': six.text_type(prov_loc), + 'metadata': {'snap-meta-key-1': 'snap-meta-value-1', + 'snap-meta-key-2': 'snap-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}} self.assertEqual(updates_response, updates) @@ -2814,3 +2870,113 @@ class PowerMaxCommonTest(test.TestCase): exception.VolumeBackendAPIException, self.common._unlink_targets_and_delete_temp_snapvx, array, session, extra_specs) + + @mock.patch.object(rest.PowerMaxRest, '_get_private_volume', + return_value=tpd.PowerMaxData.priv_vol_response_rep) + @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', + return_value=(tpd.PowerMaxData.array_model, None)) + @mock.patch.object(rest.PowerMaxRest, 'get_rdf_group', + return_value=(tpd.PowerMaxData.rdf_group_details)) + def test_get_volume_metadata_rep(self, mck_rdf, mck_model, mck_priv): + ref_metadata = { + 'DeviceID': self.data.device_id, + 'DeviceLabel': self.data.device_label, 'ArrayID': self.data.array, + 'ArrayModel': self.data.array_model, 'ServiceLevel': 'None', + 'Workload': 'None', 'Emulation': 'FBA', 'Configuration': 'TDEV', + 'CompressionEnabled': 'False', 'ReplicationEnabled': 'True', + 'R2-DeviceID': self.data.device_id2, + 'R2-ArrayID': self.data.remote_array, + 'R2-ArrayModel': self.data.array_model, + 'ReplicationMode': 'Synchronized', + 'RDFG-Label': self.data.rdf_group_name, + 'R1-RDFG': 1, 'R2-RDFG': 1} + array = self.data.array + device_id = self.data.device_id + act_metadata = self.common.get_volume_metadata(array, device_id) + self.assertEqual(ref_metadata, act_metadata) + + @mock.patch.object(rest.PowerMaxRest, '_get_private_volume', + return_value=tpd.PowerMaxData.priv_vol_response_no_rep) + @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', + return_value=(tpd.PowerMaxData.array_model, None)) + def test_get_volume_metadata_no_rep(self, mck_model, mck_priv): + ref_metadata = { + 'DeviceID': self.data.device_id, + 'DeviceLabel': self.data.device_label, 'ArrayID': self.data.array, + 'ArrayModel': self.data.array_model, 'ServiceLevel': 'None', + 'Workload': 'None', 'Emulation': 'FBA', 'Configuration': 'TDEV', + 'CompressionEnabled': 'False', 'ReplicationEnabled': 'False'} + array = self.data.array + device_id = self.data.device_id + act_metadata = self.common.get_volume_metadata(array, device_id) + self.assertEqual(ref_metadata, act_metadata) + + @mock.patch.object(rest.PowerMaxRest, 'get_volume_snap_info', + return_value=tpd.PowerMaxData.priv_snap_response) + def test_get_snapshot_metadata(self, mck_snap): + array = self.data.array + device_id = self.data.device_id + device_label = self.data.managed_snap_id + snap_name = self.data.test_snapshot_snap_name + ref_metadata = {'SnapshotLabel': snap_name, + 'SourceDeviceID': device_id, + 'SourceDeviceLabel': device_label} + + act_metadata = self.common.get_snapshot_metadata( + array, device_id, snap_name) + self.assertEqual(ref_metadata, act_metadata) + + def test_update_metadata(self): + model_update = {'provider_location': six.text_type( + self.data.provider_location)} + ref_model_update = ( + {'provider_location': six.text_type(self.data.provider_location), + 'metadata': {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}}) + + existing_metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} + + object_metadata = {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'} + + model_update = self.common.update_metadata( + model_update, existing_metadata, object_metadata) + self.assertEqual(ref_model_update, model_update) + + def test_update_metadata_no_model(self): + model_update = None + ref_model_update = ( + {'metadata': {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2', + 'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'}}) + + existing_metadata = {'user-meta-key-1': 'user-meta-value-1', + 'user-meta-key-2': 'user-meta-value-2'} + + object_metadata = {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'} + + model_update = self.common.update_metadata( + model_update, existing_metadata, object_metadata) + self.assertEqual(ref_model_update, model_update) + + def test_update_metadata_no_existing_metadata(self): + model_update = {'provider_location': six.text_type( + self.data.provider_location)} + ref_model_update = ( + {'provider_location': six.text_type(self.data.provider_location), + 'metadata': {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'}}) + + existing_metadata = None + + object_metadata = {'device-meta-key-1': 'device-meta-value-1', + 'device-meta-key-2': 'device-meta-value-2'} + + model_update = self.common.update_metadata( + model_update, existing_metadata, object_metadata) + self.assertEqual(ref_model_update, model_update) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py index 0e2caa5cd0c..c1051a9dfaf 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_replication.py @@ -125,8 +125,11 @@ class PowerMaxReplicationTest(test.TestCase): return_value=({ 'replication_driver_data': tpd.PowerMaxData.test_volume.replication_driver_data}, {})) - def test_create_replicated_volume(self, mock_rep, mock_add, mock_match, - mock_check, mock_get, mock_cg): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_replicated_volume( + self, mck_meta, mock_rep, mock_add, mock_match, mock_check, + mock_get, mock_cg): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f vol_identifier = self.utils.get_volume_element_name( @@ -149,8 +152,10 @@ class PowerMaxReplicationTest(test.TestCase): return_value=True) @mock.patch.object(rest.PowerMaxRest, 'get_rdf_group_number', side_effect=['4', None]) + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') def test_create_replicated_vol_side_effect( - self, mock_rdf_no, mock_rep_enabled, mock_rep_vol): + self, mck_meta, mock_rdf_no, mock_rep_enabled, mock_rep_vol): self.common.rep_config = self.utils.get_replication_config( [self.replication_device]) ref_rep_data = {'array': six.text_type(self.data.remote_array), @@ -158,7 +163,8 @@ class PowerMaxReplicationTest(test.TestCase): ref_model_update = { 'provider_location': six.text_type( self.data.test_volume.provider_location), - 'replication_driver_data': six.text_type(ref_rep_data)} + 'replication_driver_data': six.text_type(ref_rep_data), + 'metadata': ''} model_update = self.common.create_volume(self.data.test_volume) self.assertEqual(ref_model_update, model_update) self.assertRaises(exception.VolumeBackendAPIException, @@ -166,7 +172,9 @@ class PowerMaxReplicationTest(test.TestCase): self.data.test_volume) @mock.patch.object(common.PowerMaxCommon, '_clone_check') - def test_create_cloned_replicated_volume(self, mck_clone): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_cloned_replicated_volume(self, mck_meta, mck_clone): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f with mock.patch.object(self.common, '_replicate_volume', @@ -179,7 +187,9 @@ class PowerMaxReplicationTest(test.TestCase): self.data.test_clone_volume.name, volume_dict, extra_specs) @mock.patch.object(common.PowerMaxCommon, '_clone_check') - def test_create_replicated_volume_from_snap(self, mck_clone): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_create_replicated_volume_from_snap(self, mck_meta, mck_clone): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f with mock.patch.object(self.common, '_replicate_volume', @@ -343,7 +353,10 @@ class PowerMaxReplicationTest(test.TestCase): return_value=({}, {})) @mock.patch.object(rest.PowerMaxRest, 'get_array_model_info', return_value=('VMAX250F', False)) - def test_manage_existing_is_replicated(self, mock_model, mock_rep): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_manage_existing_is_replicated(self, mck_meta, mock_model, + mock_rep): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f external_ref = {u'source-name': u'00002'} @@ -708,7 +721,9 @@ class PowerMaxReplicationTest(test.TestCase): rep_config, array_info) self.assertEqual(ref_info, secondary_info) - def test_replicate_group(self): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_replicate_group(self, mck_meta): volume_model_update = { 'id': self.data.test_volume.id, 'provider_location': self.data.test_volume.provider_location} @@ -721,7 +736,8 @@ class PowerMaxReplicationTest(test.TestCase): 'id': self.data.test_volume.id, 'provider_location': self.data.test_volume.provider_location, 'replication_driver_data': ref_rep_data, - 'replication_status': fields.ReplicationStatus.ENABLED} + 'replication_status': fields.ReplicationStatus.ENABLED, + 'metadata': ''} # Decode string representations of dicts into dicts, because # the string representations are randomly ordered and therefore @@ -934,9 +950,11 @@ class PowerMaxReplicationTest(test.TestCase): '_remove_vol_and_cleanup_replication') @mock.patch.object(utils.PowerMaxUtils, 'is_replication_enabled', side_effect=[False, True, True, False, True, True]) - def test_migrate_volume_replication(self, mock_re, mock_rm_rep, - mock_setup, mock_retype, - mock_rm, mock_rt): + @mock.patch.object(common.PowerMaxCommon, 'get_volume_metadata', + return_value='') + def test_migrate_volume_replication( + self, mck_meta, mock_re, mock_rm_rep, mock_setup, mock_retype, + mock_rm, mock_rt): new_type = {'extra_specs': {}} for x in range(0, 3): success, model_update = self.common._migrate_volume( diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py index 1bfef219557..467d5184273 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powermax/test_powermax_utils.py @@ -540,3 +540,23 @@ class PowerMaxUtilsTest(test.TestCase): self.assertRaises(exception.VolumeBackendAPIException, self.utils.compare_cylinders, source_cylinders, target_cylinders) + + def test_get_grp_volume_model_update(self): + volume = self.data.test_volume + volume_dict = self.data.provider_location + group_id = self.data.gvg_group_id + metadata = self.data.volume_metadata + + ref_model_update_meta = { + 'id': volume.id, 'status': 'available', 'metadata': metadata, + 'provider_location': six.text_type(volume_dict)} + act_model_update_meta = self.utils.get_grp_volume_model_update( + volume, volume_dict, group_id, metadata) + self.assertEqual(ref_model_update_meta, act_model_update_meta) + + ref_model_update_no_meta = { + 'id': volume.id, 'status': 'available', + 'provider_location': six.text_type(volume_dict)} + act_model_update_no_meta = self.utils.get_grp_volume_model_update( + volume, volume_dict, group_id) + self.assertEqual(ref_model_update_no_meta, act_model_update_no_meta) diff --git a/cinder/volume/drivers/dell_emc/powermax/common.py b/cinder/volume/drivers/dell_emc/powermax/common.py index 5a57ea8914d..a7b27c13274 100644 --- a/cinder/volume/drivers/dell_emc/powermax/common.py +++ b/cinder/volume/drivers/dell_emc/powermax/common.py @@ -449,8 +449,12 @@ class PowerMaxCommon(object): group_name = self._add_new_volume_to_volume_group( volume, volume_dict['device_id'], volume_name, extra_specs, rep_driver_data) + model_update.update( {'provider_location': six.text_type(volume_dict)}) + model_update = self.update_metadata( + model_update, volume.metadata, self.get_volume_metadata( + volume_dict['array'], volume_dict['device_id'])) self.volume_metadata.capture_create_volume( volume_dict['device_id'], volume, group_name, group_id, @@ -515,7 +519,9 @@ class PowerMaxCommon(object): model_update.update( {'provider_location': six.text_type(clone_dict)}) - + model_update = self.update_metadata( + model_update, volume.metadata, self.get_volume_metadata( + clone_dict['array'], clone_dict['device_id'])) self.volume_metadata.capture_create_volume( clone_dict['device_id'], volume, None, None, extra_specs, rep_info_dict, 'createFromSnapshot', @@ -543,6 +549,9 @@ class PowerMaxCommon(object): model_update.update( {'provider_location': six.text_type(clone_dict)}) + model_update = self.update_metadata( + model_update, clone_volume.metadata, self.get_volume_metadata( + clone_dict['array'], clone_dict['device_id'])) self.volume_metadata.capture_create_volume( clone_dict['device_id'], clone_volume, None, None, extra_specs, rep_info_dict, 'createFromVolume', @@ -599,9 +608,19 @@ class PowerMaxCommon(object): extra_specs = self._initial_setup(volume) snapshot_dict = self._create_cloned_volume( snapshot, volume, extra_specs, is_snapshot=True) + + model_update = { + 'provider_location': six.text_type(snapshot_dict)} + model_update = self.update_metadata( + model_update, snapshot.metadata, self.get_snapshot_metadata( + extra_specs['array'], snapshot_dict['source_id'], + snapshot_dict['snap_name'])) + if snapshot.metadata: + model_update['metadata'].update(snapshot.metadata) + self.volume_metadata.capture_snapshot_info( volume, extra_specs, 'createSnapshot', snapshot_dict['snap_name']) - model_update = {'provider_location': six.text_type(snapshot_dict)} + return model_update def delete_snapshot(self, snapshot, volume): @@ -2463,6 +2482,10 @@ class PowerMaxCommon(object): raise exception.VolumeBackendAPIException( message=exception_message) + model_update = self.update_metadata( + model_update, volume.metadata, self.get_volume_metadata( + array, device_id)) + self.volume_metadata.capture_manage_existing( volume, rep_info_dict, device_id, extra_specs) @@ -2683,9 +2706,12 @@ class PowerMaxCommon(object): message=exception_message) prov_loc = {'source_id': device_id, 'snap_name': snap_backend_name} - - updates = {'display_name': snap_display_name, - 'provider_location': six.text_type(prov_loc)} + model_update = { + 'display_name': snap_display_name, + 'provider_location': six.text_type(prov_loc)} + model_update = self.update_metadata( + model_update, snapshot.metadata, self.get_snapshot_metadata( + array, device_id, snap_backend_name)) LOG.info("Managing SnapVX Snapshot %(snap_name)s of source " "volume %(device_id)s, OpenStack Snapshot display name: " @@ -2693,7 +2719,7 @@ class PowerMaxCommon(object): 'snap_name': snap_name, 'device_id': device_id, 'snap_display_name': snap_display_name}) - return updates + return model_update def manage_existing_snapshot_get_size(self, snapshot): """Return the size of the source volume for manage-existing-snapshot. @@ -3176,6 +3202,10 @@ class PowerMaxCommon(object): model_update = { 'replication_status': rep_status, 'replication_driver_data': six.text_type(rdf_dict)} + model_update = self.update_metadata( + model_update, volume.metadata, + self.get_volume_metadata(array, device_id)) + return True, model_update try: @@ -3198,6 +3228,10 @@ class PowerMaxCommon(object): rep_mode, is_rep_enabled, target_extra_specs) if success: + model_update = self.update_metadata( + model_update, volume.metadata, + self.get_volume_metadata(array, device_id)) + self.volume_metadata.capture_retype_info( volume, device_id, array, srp, target_slo, target_workload, target_sg_name, is_rep_enabled, rep_mode, @@ -4411,11 +4445,18 @@ class PowerMaxCommon(object): for snapshot in snapshots: src_dev_id = self._get_src_device_id_for_group_snap(snapshot) + extra_specs = self._initial_setup(snapshot.volume) + array = extra_specs['array'] + snapshots_model_update.append( {'id': snapshot.id, 'provider_location': six.text_type( {'source_id': src_dev_id, 'snap_name': snap_name}), 'status': fields.SnapshotStatus.AVAILABLE}) + snapshots_model_update = self.update_metadata( + snapshots_model_update, snapshot.metadata, + self.get_snapshot_metadata( + array, src_dev_id, snap_name)) model_update = {'status': fields.GroupStatus.AVAILABLE} return model_update, snapshots_model_update @@ -4851,7 +4892,10 @@ class PowerMaxCommon(object): (device_id, extra_specs, volume)) volumes_model_update.append( self.utils.get_grp_volume_model_update( - volume, volume_dict, group_id)) + volume, volume_dict, group_id, + meta=self.get_volume_metadata(volume_dict['array'], + volume_dict['device_id']))) + return volumes_model_update, rollback_dict, list_volume_pairs def _get_clone_vol_info(self, volume, source_vols, snapshots): @@ -4932,6 +4976,7 @@ class PowerMaxCommon(object): :param extra_specs: the extra specs :return: volumes_model_update """ + ret_volumes_model_update = [] rdf_group_no, remote_array = self.get_rdf_details(array) self.rest.replicate_group( array, group_name, rdf_group_no, remote_array, extra_specs) @@ -4953,7 +4998,11 @@ class PowerMaxCommon(object): volume_model_update.update( {'replication_driver_data': six.text_type(rep_update), 'replication_status': fields.ReplicationStatus.ENABLED}) - return volumes_model_update + volume_model_update = self.update_metadata( + volume_model_update, None, self.get_volume_metadata( + array, src_device_id)) + ret_volumes_model_update.append(volume_model_update) + return ret_volumes_model_update def enable_replication(self, context, group, volumes): """Enable replication for a group. @@ -5279,3 +5328,89 @@ class PowerMaxCommon(object): LOG.error(exception_message) raise exception.VolumeBackendAPIException( message=exception_message) + + def update_metadata( + self, model_update, existing_metadata, object_metadata): + """Update volume metadata in model_update. + + :param model_update: existing model + :param existing_metadata: existing metadata + :param object_metadata: object metadata + :returns: dict -- updated model + """ + if model_update: + if 'metadata' in model_update: + model_update['metadata'].update(object_metadata) + else: + model_update.update({'metadata': object_metadata}) + else: + model_update = {} + model_update.update({'metadata': object_metadata}) + + if existing_metadata: + model_update['metadata'].update(existing_metadata) + + return model_update + + def get_volume_metadata(self, array, device_id): + """Get volume metadata for model_update. + + :param array: the array ID + :param device_id: the device ID + :returns: dict -- volume metadata + """ + vol_info = self.rest._get_private_volume(array, device_id) + vol_header = vol_info['volumeHeader'] + array_model, __ = self.rest.get_array_model_info(array) + sl = (vol_header['serviceLevel'] if + vol_header.get('serviceLevel') else 'None') + wl = vol_header['workload'] if vol_header.get('workload') else 'None' + ce = 'True' if vol_header.get('compressionEnabled') else 'False' + + metadata = {'DeviceID': device_id, + 'DeviceLabel': vol_header['userDefinedIdentifier'], + 'ArrayID': array, 'ArrayModel': array_model, + 'ServiceLevel': sl, 'Workload': wl, + 'Emulation': vol_header['emulationType'], + 'Configuration': vol_header['configuration'], + 'CompressionEnabled': ce} + + is_rep_enabled = vol_info['rdfInfo']['RDF'] + if is_rep_enabled: + rdf_info = vol_info['rdfInfo'] + rdf_session = rdf_info['RDFSession'][0] + rdf_num = rdf_session['SRDFGroupNumber'] + rdfg_info = self.rest.get_rdf_group(array, str(rdf_num)) + r2_array_model, __ = self.rest.get_array_model_info( + rdf_session['remoteSymmetrixID']) + + metadata.update( + {'ReplicationEnabled': 'True', + 'R2-DeviceID': rdf_session['remoteDeviceID'], + 'R2-ArrayID': rdf_session['remoteSymmetrixID'], + 'R2-ArrayModel': r2_array_model, + 'ReplicationMode': rdf_session['SRDFReplicationMode'], + 'RDFG-Label': rdfg_info['label'], + 'R1-RDFG': rdf_session['SRDFGroupNumber'], + 'R2-RDFG': rdf_session['SRDFRemoteGroupNumber']}) + else: + metadata['ReplicationEnabled'] = 'False' + + return metadata + + def get_snapshot_metadata(self, array, device_id, snap_name): + """Get snapshot metadata for model_update. + + :param array: the array ID + :param device_id: the device ID + :param snap_name: the snapshot name + :returns: dict -- volume metadata + """ + snap_info = self.rest.get_volume_snap_info(array, device_id) + device_name = snap_info['deviceName'] + device_label = device_name.split(':')[1] + metadata = {'SnapshotLabel': snap_name, + 'SourceDeviceID': device_id, + 'SourceDeviceLabel': device_label} + + return metadata diff --git a/cinder/volume/drivers/dell_emc/powermax/fc.py b/cinder/volume/drivers/dell_emc/powermax/fc.py index 85496046197..ea057ee31c5 100644 --- a/cinder/volume/drivers/dell_emc/powermax/fc.py +++ b/cinder/volume/drivers/dell_emc/powermax/fc.py @@ -114,6 +114,7 @@ class PowerMaxFCDriver(san.SanDriver, driver.FibreChannelDriver): - Support for Metro ODE (bp/powermax-metro-ode) - Removal of san_rest_port from PowerMax cinder.conf config - SnapVX noCopy mode enabled for all links + - Volume/Snapshot backed metadata inclusion """ VERSION = "4.1.0" diff --git a/cinder/volume/drivers/dell_emc/powermax/iscsi.py b/cinder/volume/drivers/dell_emc/powermax/iscsi.py index 47cdc2c976c..9134981b766 100644 --- a/cinder/volume/drivers/dell_emc/powermax/iscsi.py +++ b/cinder/volume/drivers/dell_emc/powermax/iscsi.py @@ -119,6 +119,7 @@ class PowerMaxISCSIDriver(san.SanISCSIDriver): - Support for Metro ODE (bp/powermax-metro-ode) - Removal of san_rest_port from PowerMax cinder.conf config - SnapVX noCopy mode enabled for all links + - Volume/Snapshot backed metadata inclusion """ VERSION = "4.1.0" diff --git a/cinder/volume/drivers/dell_emc/powermax/utils.py b/cinder/volume/drivers/dell_emc/powermax/utils.py index a77f7e4adf6..5507ca47afe 100644 --- a/cinder/volume/drivers/dell_emc/powermax/utils.py +++ b/cinder/volume/drivers/dell_emc/powermax/utils.py @@ -508,17 +508,20 @@ class PowerMaxUtils(object): return volume_model_updates @staticmethod - def get_grp_volume_model_update(volume, volume_dict, group_id): + def get_grp_volume_model_update(volume, volume_dict, group_id, meta=None): """Create and return the volume model update on creation. :param volume: volume object :param volume_dict: the volume dict :param group_id: consistency group id + :param meta: the volume metadata :returns: model_update """ LOG.info("Updating status for group: %(id)s.", {'id': group_id}) model_update = ({'id': volume.id, 'status': 'available', 'provider_location': six.text_type(volume_dict)}) + if meta: + model_update['metadata'] = meta return model_update @staticmethod diff --git a/releasenotes/notes/powermax-vol-metadata-acd2555818d25b72.yaml b/releasenotes/notes/powermax-vol-metadata-acd2555818d25b72.yaml new file mode 100644 index 00000000000..1304328d6e2 --- /dev/null +++ b/releasenotes/notes/powermax-vol-metadata-acd2555818d25b72.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + All volumes and snapshots created using the PowerMax for Cinder driver now + have additional metadata included pertaining to the details of the asset on + the backend storage array.