From 4662ead8c3ef1970bc7be7815bcbacc221f6fe1e Mon Sep 17 00:00:00 2001 From: Helen Walsh Date: Wed, 28 Mar 2018 18:08:00 +0100 Subject: [PATCH] VMAX driver - Add VMAX specific volume metadata to logs Log VMAX specific metadata of a volume and version information, when debug is enabled. It enables the user to trace the vmax location of a volume after every operation i.e. storage group, masking view etc., as well as useful debug information like OS, VMAX and python versions. Change-Id: Ib727797da7624dec5662a35de1db05ad6dc866a0 Implements: blueprint vmax-metadata --- .../volume/drivers/dell_emc/vmax/test_vmax.py | 288 +++++++- cinder/volume/drivers/dell_emc/vmax/common.py | 158 ++++- cinder/volume/drivers/dell_emc/vmax/fc.py | 1 + cinder/volume/drivers/dell_emc/vmax/iscsi.py | 1 + .../volume/drivers/dell_emc/vmax/metadata.py | 653 ++++++++++++++++++ cinder/volume/drivers/dell_emc/vmax/rest.py | 14 +- cinder/volume/drivers/dell_emc/vmax/utils.py | 30 + .../notes/vmax-metadata-ac9bdd31e7e561c3.yaml | 4 + 8 files changed, 1106 insertions(+), 43 deletions(-) create mode 100644 cinder/volume/drivers/dell_emc/vmax/metadata.py create mode 100644 releasenotes/notes/vmax-metadata-ac9bdd31e7e561c3.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py b/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py index 1f3ce2e4346..33c8fea10e4 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vmax/test_vmax.py @@ -16,6 +16,7 @@ import ast from copy import deepcopy import datetime +import platform import time import mock @@ -35,10 +36,12 @@ from cinder.tests.unit import fake_group from cinder.tests.unit import fake_snapshot from cinder.tests.unit import fake_volume from cinder.tests.unit import utils as test_utils +from cinder import version as openstack_version from cinder.volume.drivers.dell_emc.vmax import common from cinder.volume.drivers.dell_emc.vmax import fc from cinder.volume.drivers.dell_emc.vmax import iscsi from cinder.volume.drivers.dell_emc.vmax import masking +from cinder.volume.drivers.dell_emc.vmax import metadata from cinder.volume.drivers.dell_emc.vmax import provision from cinder.volume.drivers.dell_emc.vmax import rest from cinder.volume.drivers.dell_emc.vmax import utils @@ -909,6 +912,42 @@ class VMAXCommonData(object): "getDynamicRDFCapability": "RDF1_Capable", "RDFA": False}, "timeFinderInfo": {"snapVXTgt": False, "snapVXSrc": False}}] + volume_info_dict = { + 'volume_id': volume_id, + 'service_level': 'Diamond', + 'masking_view': 'OS-HostX-F-OS-fibre-PG-MV', + 'host': fake_host, + 'display_name': 'attach_vol_name', + 'volume_updated_time': '2018-03-05 20:32:41', + 'port_group': 'OS-fibre-PG', + 'operation': 'attach', 'srp': 'SRP_1', + 'initiator_group': 'OS-HostX-F-IG', + 'serial_number': '000197800123', + 'parent_storage_group': 'OS-HostX-F-OS-fibre-PG-SG', + 'workload': 'DSS', + 'child_storage_group': 'OS-HostX-SRP_1-DiamondDSS-OS-fibre-PG'} + + data_dict = {volume_id: volume_info_dict} + platform = 'Linux-4.4.0-104-generic-x86_64-with-Ubuntu-16.04-xenial' + unisphere_version = u'V9.0.0.1' + openstack_release = '12.0.0.0b3.dev401' + openstack_version = '12.0.0' + python_version = '2.7.12' + vmax_driver_version = '3.1' + vmax_firmware_version = u'5977.1125.1125' + vmax_model = u'VMAX250F' + + version_dict = { + 'unisphere_version': unisphere_version, + 'openstack_release': openstack_release, + 'openstack_version': openstack_version, + 'python_version': python_version, + 'vmax_driver_version': vmax_driver_version, + 'platform': platform, + 'vmax_firmware_version': vmax_firmware_version, + 'serial_number': array, + 'vmax_model': vmax_model} + class FakeLookupService(object): def get_device_mapping_from_network(self, initiator_wwns, target_wwns): @@ -7197,9 +7236,9 @@ class VMAXCommonReplicationTest(test.TestCase): @mock.patch.object(masking.VMAXMasking, 'add_volume_to_storage_group') @mock.patch.object( common.VMAXCommon, '_replicate_volume', - return_value={ + return_value=({ 'replication_driver_data': - VMAXCommonData.test_volume.replication_driver_data}) + VMAXCommonData.test_volume.replication_driver_data}, {})) def test_create_replicated_volume(self, mock_rep, mock_add, mock_match, mock_check, mock_get, mock_cg): extra_specs = deepcopy(self.extra_specs) @@ -7219,7 +7258,7 @@ class VMAXCommonReplicationTest(test.TestCase): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f with mock.patch.object(self.common, '_replicate_volume', - return_value={}) as mock_rep: + return_value=({}, {})) as mock_rep: self.common.create_cloned_volume( self.data.test_clone_volume, self.data.test_volume) volume_dict = self.data.provider_location @@ -7231,7 +7270,7 @@ class VMAXCommonReplicationTest(test.TestCase): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f with mock.patch.object(self.common, '_replicate_volume', - return_value={}) as mock_rep: + return_value=({}, {})) as mock_rep: self.common.create_volume_from_snapshot( self.data.test_clone_volume, self.data.test_snapshot) volume_dict = self.data.provider_location @@ -7245,7 +7284,8 @@ class VMAXCommonReplicationTest(test.TestCase): volume_dict = self.data.provider_location rs_enabled = fields.ReplicationStatus.ENABLED with mock.patch.object(self.common, 'setup_volume_replication', - return_value=(rs_enabled, {})) as mock_setup: + return_value=(rs_enabled, {}, {}))\ + as mock_setup: self.common._replicate_volume( self.data.test_volume, "1", volume_dict, self.extra_specs) mock_setup.assert_called_once_with( @@ -7396,7 +7436,7 @@ class VMAXCommonReplicationTest(test.TestCase): @mock.patch.object(common.VMAXCommon, '_replicate_volume', - return_value={}) + return_value=({}, {})) def test_manage_existing_is_replicated(self, mock_rep): extra_specs = deepcopy(self.extra_specs) extra_specs[utils.PORTGROUPNAME] = self.data.port_group_name_f @@ -7414,7 +7454,7 @@ class VMAXCommonReplicationTest(test.TestCase): @mock.patch.object(masking.VMAXMasking, 'remove_and_reset_members') def test_setup_volume_replication(self, mock_rm): - rep_status, rep_data = self.common.setup_volume_replication( + rep_status, rep_data, __ = self.common.setup_volume_replication( self.data.array, self.data.test_volume, self.data.device_id, self.extra_specs) self.assertEqual(fields.ReplicationStatus.ENABLED, rep_status) @@ -7424,7 +7464,7 @@ class VMAXCommonReplicationTest(test.TestCase): @mock.patch.object(masking.VMAXMasking, 'remove_and_reset_members') @mock.patch.object(common.VMAXCommon, '_create_volume') def test_setup_volume_replication_target(self, mock_create, mock_rm): - rep_status, rep_data = self.common.setup_volume_replication( + rep_status, rep_data, __ = self.common.setup_volume_replication( self.data.array, self.data.test_volume, self.data.device_id, self.extra_specs, self.data.device_id2) self.assertEqual(fields.ReplicationStatus.ENABLED, rep_status) @@ -7911,7 +7951,7 @@ class VMAXCommonReplicationTest(test.TestCase): def test_setup_volume_replication_async(self, mock_rm, mock_add): extra_specs = deepcopy(self.extra_specs) extra_specs['rep_mode'] = utils.REP_ASYNC - rep_status, rep_data = ( + rep_status, rep_data, __ = ( self.async_driver.common.setup_volume_replication( self.data.array, self.data.test_volume, self.data.device_id, extra_specs)) @@ -7936,7 +7976,8 @@ class VMAXCommonReplicationTest(test.TestCase): @mock.patch.object(common.VMAXCommon, '_retype_remote_volume', return_value=True) @mock.patch.object(common.VMAXCommon, 'setup_volume_replication', - return_value=VMAXCommonData.provider_location2) + return_value=( + '', VMAXCommonData.provider_location2, '')) @mock.patch.object(common.VMAXCommon, '_remove_vol_and_cleanup_replication') @mock.patch.object(utils.VMAXUtils, 'is_replication_enabled', @@ -7970,3 +8011,230 @@ class VMAXCommonReplicationTest(test.TestCase): self.data.test_volume.name, utils.REP_SYNC, True, self.data.extra_specs) mock_retype.assert_called_once() + + +class VMAXVolumeMetadataNoDebugTest(test.TestCase): + def setUp(self): + self.data = VMAXCommonData() + + super(VMAXVolumeMetadataNoDebugTest, self).setUp() + is_debug = False + self.volume_metadata = metadata.VMAXVolumeMetadata( + rest.VMAXRest, '3.1', is_debug) + + @mock.patch.object(metadata.VMAXVolumeMetadata, '_fill_volume_trace_dict', + return_value={}) + def test_gather_volume_info(self, mock_fvtd): + self.volume_metadata.gather_volume_info( + self.data.volume_id, 'create', False, volume_size=1) + mock_fvtd.assert_not_called() + + +class VMAXVolumeMetadataDebugTest(test.TestCase): + def setUp(self): + self.data = VMAXCommonData() + + super(VMAXVolumeMetadataDebugTest, self).setUp() + is_debug = True + self.volume_metadata = metadata.VMAXVolumeMetadata( + rest.VMAXRest, '3.1', is_debug) + self.utils = self.volume_metadata.utils + self.rest = self.volume_metadata.rest + + @mock.patch.object(metadata.VMAXVolumeMetadata, '_fill_volume_trace_dict', + return_value={}) + def test_gather_volume_info(self, mock_fvtd): + self.volume_metadata.gather_volume_info( + self.data.volume_id, 'create', False, volume_size=1) + mock_fvtd.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_attach_info(self, mock_uvim): + self.volume_metadata.capture_attach_info( + self.data.test_volume, self.data.extra_specs, + self.data.masking_view_dict, self.data.fake_host, + False, False) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_create_volume(self, mock_uvim): + self.volume_metadata.capture_create_volume( + self.data.device_id, self.data.test_volume, "test_group", + "test_group_id", self.data.extra_specs, {}, 'create', None) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_manage_existing(self, mock_uvim): + self.volume_metadata.capture_manage_existing( + self.data.test_volume, {}, self.data.device_id, + self.data.extra_specs) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_failover_volume(self, mock_uvim): + self.volume_metadata.capture_failover_volume( + self.data.test_volume, self.data.device_id2, + self.data.remote_array, self.data.rdf_group_name, + self.data.device_id, self.data.array, + self.data.extra_specs, True, None, + fields.ReplicationStatus.FAILED_OVER, utils.REP_SYNC) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_modify_group(self, mock_uvim): + self.volume_metadata.capture_modify_group( + "test_group", "test_group_id", [self.data.test_volume], + [], self.data.array) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_extend_info(self, mock_uvim): + self.volume_metadata.capture_extend_info( + self.data.test_volume, 5, self.data.device_id, + self.data.extra_specs, self.data.array) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_detach_info(self, mock_uvim): + self.volume_metadata.capture_detach_info( + self.data.test_volume, self.data.extra_specs, self.data.device_id, + None, None) + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_snapshot_info(self, mock_uvim): + self.volume_metadata.capture_snapshot_info( + self.data.test_volume, self.data.extra_specs, 'createSnapshot', + 'ss-test-vol') + mock_uvim.assert_called_once() + + @mock.patch.object(metadata.VMAXVolumeMetadata, + 'update_volume_info_metadata', + return_value={}) + def test_capture_retype_info(self, mock_uvim): + self.volume_metadata.capture_retype_info( + self.data.test_volume.id, 20, self.data.device_id, self.data.array, + self.data.srp, self.data.slo, self.data.workload, + self.data.storagegroup_name_target, False, None, + False) + mock_uvim.assert_called_once() + + def test_update_volume_info_metadata(self): + volume_metadata = self.volume_metadata.update_volume_info_metadata( + self.data.data_dict, self.data.version_dict) + self.assertEqual('2.7.12', volume_metadata['python_version']) + self.assertEqual('VMAX250F', volume_metadata['vmax_model']) + self.assertEqual('DSS', volume_metadata['workload']) + self.assertEqual('OS-fibre-PG', volume_metadata['port_group']) + + def test_fill_volume_trace_dict(self): + datadict = {} + volume_trace_dict = {} + volume_key_value = {} + result_dict = {'operation': 'create', + 'volume_id': self.data.test_volume.id} + volume_metadata = self.volume_metadata._fill_volume_trace_dict( + self.data.test_volume.id, 'create', False, target_name=None, + datadict=datadict, volume_key_value=volume_key_value, + volume_trace_dict=volume_trace_dict) + self.assertEqual(result_dict, volume_metadata) + + def test_fill_volume_trace_dict_multi_attach(self): + mv_list = ['mv1', 'mv2', 'mv3'] + sg_list = ['sg1', 'sg2', 'sg3'] + datadict = {} + volume_trace_dict = {} + volume_key_value = {} + result_dict = {'masking_view_1': 'mv1', + 'masking_view_2': 'mv2', + 'masking_view_3': 'mv3', + 'operation': 'attach', + 'storage_group_1': 'sg1', + 'storage_group_2': 'sg2', + 'storage_group_3': 'sg3', + 'volume_id': self.data.test_volume.id} + volume_metadata = self.volume_metadata._fill_volume_trace_dict( + self.data.test_volume.id, 'attach', False, target_name=None, + datadict=datadict, volume_trace_dict=volume_trace_dict, + volume_key_value=volume_key_value, mv_list=mv_list, + sg_list=sg_list) + self.assertEqual(result_dict, volume_metadata) + + @mock.patch.object(utils.VMAXUtils, 'merge_dicts', + return_value={}) + def test_consolidate_volume_trace_list(self, mock_m2d): + self.volume_metadata.volume_trace_list = [self.data.data_dict] + volume_trace_dict = {'volume_updated_time': '2018-03-06 16:51:40', + 'operation': 'delete', + 'volume_id': self.data.volume_id} + volume_key_value = {self.data.volume_id: volume_trace_dict} + self.volume_metadata._consolidate_volume_trace_list( + self.data.volume_id, volume_trace_dict, volume_key_value) + mock_m2d.assert_called_once() + + def test_merge_dicts_multiple(self): + d1 = {'a': 1, 'b': 2} + d2 = {'c': 3, 'd': 4} + d3 = {'e': 5, 'f': 6} + res_d = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6} + result_dict = self.utils.merge_dicts( + d1, d2, d3) + self.assertEqual(res_d, result_dict) + + def test_merge_dicts_multiple_2(self): + d1 = {'a': 1, 'b': 2} + d2 = {'b': 3, 'd': 4} + d3 = {'d': 5, 'e': 6} + res_d = {'a': 1, 'b': 2, 'd': 4, 'e': 6} + result_dict = self.utils.merge_dicts( + d1, d2, d3) + self.assertEqual(res_d, result_dict) + + def test_merge_dicts(self): + self.volume_metadata.volume_trace_list = [self.data.data_dict] + volume_trace_dict = {'volume_updated_time': '2018-03-06 16:51:40', + 'operation': 'delete', + 'volume_id': self.data.volume_id} + result_dict = self.utils.merge_dicts( + volume_trace_dict, self.data.volume_info_dict) + self.assertEqual('delete', result_dict['operation']) + self.assertEqual( + '2018-03-06 16:51:40', result_dict['volume_updated_time']) + self.assertEqual('OS-fibre-PG', result_dict['port_group']) + + @mock.patch.object(platform, 'platform', + return_value=VMAXCommonData.platform) + @mock.patch.object(platform, 'python_version', + return_value=VMAXCommonData.python_version) + @mock.patch.object(openstack_version.version_info, 'version_string', + return_value=VMAXCommonData.openstack_version) + @mock.patch.object(openstack_version.version_info, 'release_string', + return_value=VMAXCommonData.openstack_release) + @mock.patch.object(rest.VMAXRest, 'get_unisphere_version', + return_value={ + 'version': VMAXCommonData.unisphere_version}) + @mock.patch.object(rest.VMAXRest, 'get_array_serial', + return_value={ + 'ucode': VMAXCommonData.vmax_firmware_version, + 'model': VMAXCommonData.vmax_model}) + def test_gather_version_info( + self, mock_vi, mock_ur, mock_or, mock_ov, mock_pv, mock_p): + self.volume_metadata.gather_version_info(self.data.array) + self.assertEqual( + self.data.version_dict, self.volume_metadata.version_dict) diff --git a/cinder/volume/drivers/dell_emc/vmax/common.py b/cinder/volume/drivers/dell_emc/vmax/common.py index 69bbb15656d..085d84870f3 100644 --- a/cinder/volume/drivers/dell_emc/vmax/common.py +++ b/cinder/volume/drivers/dell_emc/vmax/common.py @@ -31,6 +31,7 @@ from cinder.i18n import _ from cinder.objects import fields from cinder.volume import configuration from cinder.volume.drivers.dell_emc.vmax import masking +from cinder.volume.drivers.dell_emc.vmax import metadata as volume_metadata from cinder.volume.drivers.dell_emc.vmax import provision from cinder.volume.drivers.dell_emc.vmax import rest from cinder.volume.drivers.dell_emc.vmax import utils @@ -118,6 +119,8 @@ class VMAXCommon(object): self.masking = masking.VMAXMasking(prtcl, self.rest) self.provision = provision.VMAXProvision(self.rest) self.version = version + self.volume_metadata = volume_metadata.VMAXVolumeMetadata( + self.rest, version, LOG.isEnabledFor(logging.DEBUG)) # replication self.replication_enabled = False self.extend_replicated_vol = False @@ -126,6 +129,7 @@ class VMAXCommon(object): self.failover = False self._get_replication_info() self._gather_info() + self.version_dict = {} def _gather_info(self): """Gather the relevant information for update_volume_stats.""" @@ -249,8 +253,11 @@ class VMAXCommon(object): :returns: model_update - dict """ model_update = {} + rep_info_dict = {} rep_driver_data = {} volume_id = volume.id + group_name = None + group_id = None extra_specs = self._initial_setup(volume) if 'qos' in extra_specs: del extra_specs['qos'] @@ -264,8 +271,8 @@ class VMAXCommon(object): # Set-up volume replication, if enabled if self.utils.is_replication_enabled(extra_specs): - rep_update = self._replicate_volume(volume, volume_name, - volume_dict, extra_specs) + rep_update, rep_info_dict = self._replicate_volume( + volume, volume_name, volume_dict, extra_specs) rep_driver_data = rep_update['replication_driver_data'] model_update.update(rep_update) @@ -273,16 +280,20 @@ class VMAXCommon(object): if volume.group_id is not None: if (volume_utils.is_group_a_cg_snapshot_type(volume.group) or volume.group.is_replicated): - LOG.debug("Adding volume %(vol_id)s to group %(grp_id)s", - {'vol_id': volume.id, 'grp_id': volume.group_id}) - self._add_new_volume_to_volume_group( + group_id = volume.group_id + 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)}) + + self.volume_metadata.capture_create_volume( + volume_dict['device_id'], volume, group_name, group_id, + extra_specs, rep_info_dict, 'create', None) LOG.info("Leaving create_volume: %(name)s. Volume dict: %(dict)s.", {'name': volume_name, 'dict': volume_dict}) - model_update.update( - {'provider_location': six.text_type(volume_dict)}) + return model_update def _add_new_volume_to_volume_group(self, volume, device_id, volume_name, @@ -295,6 +306,7 @@ class VMAXCommon(object): :param volume_name: the volume name :param extra_specs: the extra specifications :param rep_driver_data: the replication driver data, optional + :returns: group_name string """ self.utils.check_replication_matched(volume, extra_specs) group_name = self.provision.get_or_create_volume_group( @@ -306,6 +318,7 @@ class VMAXCommon(object): if volume.group.is_replicated: self.masking.add_remote_vols_to_volume_group( volume, volume.group, extra_specs, rep_driver_data) + return group_name def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot. @@ -316,7 +329,7 @@ class VMAXCommon(object): :raises: VolumeBackendAPIException: """ LOG.debug("Entering create_volume_from_snapshot.") - model_update = {} + model_update, rep_info_dict = {}, {} extra_specs = self._initial_setup(volume) # Check if legacy snapshot @@ -330,12 +343,19 @@ class VMAXCommon(object): # Set-up volume replication, if enabled if self.utils.is_replication_enabled(extra_specs): - rep_update = self._replicate_volume(volume, snapshot['name'], - clone_dict, extra_specs) + rep_update, rep_info_dict = ( + self._replicate_volume( + volume, snapshot['name'], clone_dict, extra_specs)) model_update.update(rep_update) model_update.update( {'provider_location': six.text_type(clone_dict)}) + + self.volume_metadata.capture_create_volume( + clone_dict['device_id'], volume, None, None, + extra_specs, rep_info_dict, 'createFromSnapshot', + snapshot.id) + return model_update def create_cloned_volume(self, clone_volume, source_volume): @@ -345,19 +365,23 @@ class VMAXCommon(object): :param source_volume: volume object :returns: model_update, dict """ - model_update = {} + model_update, rep_info_dict = {}, {} extra_specs = self._initial_setup(clone_volume) clone_dict = self._create_cloned_volume(clone_volume, source_volume, extra_specs) # Set-up volume replication, if enabled if self.utils.is_replication_enabled(extra_specs): - rep_update = self._replicate_volume( + rep_update, rep_info_dict = self._replicate_volume( clone_volume, clone_volume.name, clone_dict, extra_specs) model_update.update(rep_update) model_update.update( {'provider_location': six.text_type(clone_dict)}) + self.volume_metadata.capture_create_volume( + clone_dict['device_id'], clone_volume, None, None, + extra_specs, rep_info_dict, 'createFromVolume', + None) return model_update def _replicate_volume(self, volume, volume_name, volume_dict, extra_specs, @@ -370,12 +394,12 @@ class VMAXCommon(object): :param extra_specs: the extra specifications :param delete_src: flag to indicate if source should be deleted on if replication fails - :returns: replication model_update + :returns: replication model_update, rep_info_dict """ array = volume_dict['array'] try: device_id = volume_dict['device_id'] - replication_status, replication_driver_data = ( + replication_status, replication_driver_data, rep_info_dict = ( self.setup_volume_replication( array, volume, device_id, extra_specs)) except Exception: @@ -385,7 +409,7 @@ class VMAXCommon(object): raise return ({'replication_status': replication_status, 'replication_driver_data': six.text_type( - replication_driver_data)}) + replication_driver_data)}, rep_info_dict) def delete_volume(self, volume): """Deletes a EMC(VMAX) volume. @@ -408,6 +432,8 @@ class VMAXCommon(object): extra_specs = self._initial_setup(volume) snapshot_dict = self._create_cloned_volume( snapshot, volume, extra_specs, is_snapshot=True) + 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 @@ -443,6 +469,8 @@ class VMAXCommon(object): LOG.info("Leaving delete_snapshot: %(ssname)s.", {'ssname': snap_name}) + self.volume_metadata.capture_snapshot_info( + volume, extra_specs, 'deleteSnapshot', None) def _remove_members(self, array, volume, device_id, extra_specs, connector, is_multiattach, @@ -474,6 +502,7 @@ class VMAXCommon(object): :param volume: the volume Object :param connector: the connector Object """ + mv_list, sg_list = None, None extra_specs = self._initial_setup(volume) if 'qos' in extra_specs: del extra_specs['qos'] @@ -535,6 +564,14 @@ class VMAXCommon(object): self.masking.attempt_ig_cleanup( connector, self.protocol, rep_extra_specs[utils.ARRAY], True) + if is_multiattach and LOG.isEnabledFor(logging.DEBUG): + mv_list, sg_list = ( + self._get_mvs_and_sgs_from_volume( + extra_specs[utils.ARRAY], + device_info['device_id'])) + self.volume_metadata.capture_detach_info( + volume, extra_specs, device_info['device_id'], mv_list, + sg_list) def initialize_connection(self, volume, connector): """Initializes the connection and returns device and connection info. @@ -640,6 +677,17 @@ class VMAXCommon(object): self._find_ip_and_iqns( rep_extra_specs[utils.ARRAY], remote_port_group)) device_info_dict['is_multipath'] = is_multipath + + if is_multiattach and LOG.isEnabledFor(logging.DEBUG): + masking_view_dict['mv_list'], masking_view_dict['sg_list'] = ( + self._get_mvs_and_sgs_from_volume( + extra_specs[utils.ARRAY], + masking_view_dict[utils.DEVICE_ID])) + + self.volume_metadata.capture_attach_info( + volume, extra_specs, masking_view_dict, connector['host'], + is_multipath, is_multiattach) + return device_info_dict def _attach_metro_volume(self, volume, connector, is_multiattach, @@ -772,6 +820,9 @@ class VMAXCommon(object): self.provision.extend_volume( array, device_id, new_size, extra_specs) + self.volume_metadata.capture_extend_info( + volume, new_size, device_id, extra_specs, array) + LOG.debug("Leaving extend_volume: %(volume_name)s. ", {'volume_name': volume_name}) @@ -1095,21 +1146,33 @@ class VMAXCommon(object): """ LOG.debug("Getting masking views from volume") host_maskingview_list, all_masking_view_list = [], [] - storage_group_list = self.rest.get_storage_groups_from_volume( - array, device_id) host_compare = True if host else False - for sg in storage_group_list: - mvs = self.rest.get_masking_views_from_storage_group( - array, sg) - for mv in mvs: - all_masking_view_list.append(mv) - if host_compare: - if host.lower() in mv.lower(): - host_maskingview_list.append(mv) + mvs, __ = self._get_mvs_and_sgs_from_volume(array, device_id) + for mv in mvs: + all_masking_view_list.append(mv) + if host_compare: + if host.lower() in mv.lower(): + host_maskingview_list.append(mv) maskingview_list = (host_maskingview_list if host_compare else all_masking_view_list) return maskingview_list, all_masking_view_list + def _get_mvs_and_sgs_from_volume(self, array, device_id): + """Helper function to retrieve masking views and storage groups. + + :param array: array serial number + :param device_id: the volume device id + :returns: masking view list, storage group list + """ + final_masking_view_list = [] + storage_group_list = self.rest.get_storage_groups_from_volume( + array, device_id) + for sg in storage_group_list: + masking_view_list = self.rest.get_masking_views_from_storage_group( + array, sg) + final_masking_view_list.extend(masking_view_list) + return final_masking_view_list, storage_group_list + def _initial_setup(self, volume, volume_type_id=None): """Necessary setup to accumulate the relevant information. @@ -1372,7 +1435,6 @@ class VMAXCommon(object): :param volume_name: the volume name :param volume_size: the volume size :param extra_specs: extra specifications - :returns: int -- return code :returns: dict -- volume_dict :raises: VolumeBackendAPIException: """ @@ -1521,6 +1583,12 @@ class VMAXCommon(object): 'array': extra_specs[utils.ARRAY], 'slo': extra_specs[utils.SLO], 'workload': extra_specs[utils.WORKLOAD]}) + if self.version_dict: + self.volume_metadata.print_pretty_table(self.version_dict) + else: + self.version_dict = ( + self.volume_metadata.gather_version_info( + extra_specs[utils.ARRAY])) return extra_specs def _delete_from_srp(self, array, device_id, volume_name, @@ -1803,6 +1871,7 @@ class VMAXCommon(object): :returns: dict -- model_update """ LOG.info("Beginning manage existing volume process") + rep_info_dict = {} array, device_id = self.utils.get_array_and_device_id( volume, external_ref) volume_id = volume.id @@ -1822,9 +1891,9 @@ class VMAXCommon(object): # Set-up volume replication, if enabled if self.utils.is_replication_enabled(extra_specs): - rep_update = self._replicate_volume(volume, volume_name, - provider_location, - extra_specs, delete_src=False) + rep_update, rep_info_dict = self._replicate_volume( + volume, volume_name, provider_location, + extra_specs, delete_src=False) model_update.update(rep_update) else: @@ -1832,6 +1901,9 @@ class VMAXCommon(object): self.masking.add_volume_to_default_storage_group( array, device_id, volume_name, extra_specs) + self.volume_metadata.capture_manage_existing( + volume, rep_info_dict, device_id, extra_specs) + return model_update def _check_lun_valid_for_cinder_management( @@ -2423,7 +2495,7 @@ class VMAXCommon(object): else: if is_rep_enabled: # Setup_volume_replication will put volume in correct sg - rep_status, rdf_dict = self.setup_volume_replication( + rep_status, rdf_dict, __ = self.setup_volume_replication( array, volume, device_id, target_extra_specs) model_update = { 'replication_status': rep_status, @@ -2447,6 +2519,11 @@ class VMAXCommon(object): array, volume, device_id, volume_name, rep_mode, is_rep_enabled, target_extra_specs) + self.volume_metadata.capture_retype_info( + volume.id, volume.size, device_id, array, srp, target_slo, + target_workload, target_sg_name, is_rep_enabled, rep_mode, + is_compression_disabled) + return success, model_update def _retype_volume(self, array, device_id, volume_name, target_sg_name, @@ -2642,7 +2719,9 @@ class VMAXCommon(object): :param extra_specs: the extra specifications :param target_device_id: the target device id :returns: replication_status -- str, replication_driver_data -- dict + rep_info_dict -- dict """ + rep_extra_specs = {'rep_mode': None} source_name = volume.name LOG.debug('Starting replication setup ' 'for volume: %s.', source_name) @@ -2686,8 +2765,15 @@ class VMAXCommon(object): target_name) replication_status = REPLICATION_ENABLED replication_driver_data = rdf_dict + rep_info_dict = self.volume_metadata.gather_replication_info( + rdf_group_no=rdf_group_no, + target_name=target_name, remote_array=remote_array, + target_device_id=target_device_id, + replication_status=replication_status, + rep_mode=rep_extra_specs['rep_mode'], + rdf_group_label=self.rep_config['rdf_group_label']) - return replication_status, replication_driver_data + return replication_status, replication_driver_data, rep_info_dict def _add_volume_to_async_rdf_managed_grp( self, array, device_id, volume_name, remote_array, @@ -3779,6 +3865,9 @@ class VMAXCommon(object): LOG.exception(exception_message) raise exception.VolumeBackendAPIException(data=exception_message) + self.volume_metadata.capture_modify_group( + vol_grp_name, group.id, add_vols, remove_volumes, array) + return model_update, None, None def _remove_remote_vols_from_volume_group( @@ -4211,6 +4300,13 @@ class VMAXCommon(object): if vol_rep_status != fields.ReplicationStatus.ERROR: loc = vol.replication_driver_data rep_data = vol.provider_location + local = ast.literal_eval(loc) + remote = ast.literal_eval(rep_data) + self.volume_metadata.capture_failover_volume( + vol, local['device_id'], local['array'], rdf_group_no, + remote['device_id'], remote['array'], extra_specs, + failover, vol_grp_name, vol_rep_status, utils.REP_ASYNC) + update = {'id': vol.id, 'replication_status': vol_rep_status, 'provider_location': loc, diff --git a/cinder/volume/drivers/dell_emc/vmax/fc.py b/cinder/volume/drivers/dell_emc/vmax/fc.py index 7bcb5bf4df5..b0f0e426545 100644 --- a/cinder/volume/drivers/dell_emc/vmax/fc.py +++ b/cinder/volume/drivers/dell_emc/vmax/fc.py @@ -96,6 +96,7 @@ class VMAXFCDriver(san.SanDriver, driver.FibreChannelDriver): - Support for list manageable volumes and snapshots (bp/vmax-list-manage-existing) - Fix for SSL verification/cert application (bug #1772924) + - Log VMAX metadata of a volume (bp vmax-metadata) """ VERSION = "3.2.0" diff --git a/cinder/volume/drivers/dell_emc/vmax/iscsi.py b/cinder/volume/drivers/dell_emc/vmax/iscsi.py index 630e02a6d91..9d0cc2366f4 100644 --- a/cinder/volume/drivers/dell_emc/vmax/iscsi.py +++ b/cinder/volume/drivers/dell_emc/vmax/iscsi.py @@ -101,6 +101,7 @@ class VMAXISCSIDriver(san.SanISCSIDriver): - Support for list manageable volumes and snapshots (bp/vmax-list-manage-existing) - Fix for SSL verification/cert application (bug #1772924) + - Log VMAX metadata of a volume (bp vmax-metadata) """ VERSION = "3.2.0" diff --git a/cinder/volume/drivers/dell_emc/vmax/metadata.py b/cinder/volume/drivers/dell_emc/vmax/metadata.py new file mode 100644 index 00000000000..05d3cf0fd4e --- /dev/null +++ b/cinder/volume/drivers/dell_emc/vmax/metadata.py @@ -0,0 +1,653 @@ +# Copyright (c) 2018 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import datetime +import platform +import prettytable +import six +import time +import types + +from oslo_log import log as logging + +from cinder.objects import volume +from cinder import version + +from cinder.volume.drivers.dell_emc.vmax import utils +LOG = logging.getLogger(__name__) +CLEANUP_LIST = ['masking_view', 'child_storage_group', 'parent_storage_group', + 'initiator_group', 'port_group', 'storage_group'] + + +def debug_required(func): + """Only execute the function if debug is enabled.""" + + def func_wrapper(*args, **kwargs): + try: + if args[0].is_debug: + return func(*args, **kwargs) + else: + pass + except Exception as ex: + LOG.warning("Volume metadata logging failure. " + "Exception is %s.", ex) + + return func_wrapper + + +class VMAXVolumeMetadata(object): + """Gathers VMAX specific volume information. + + Also gathers Unisphere, Microcode OS/distribution and python versions. + """ + + def __init__(self, rest, version, is_debug): + self.version_dict = {} + self.rest = rest + self.utils = utils.VMAXUtils() + self.volume_trace_list = [] + self.is_debug = is_debug + self.vmax_driver_version = version + + def _update_platform(self): + """Update the platform.""" + try: + self.version_dict['platform'] = platform.platform() + except Exception as ex: + LOG.warning("Unable to determine the platform. " + "Exception is %s.", ex) + + def _get_python_version(self): + """Get the python version.""" + try: + self.version_dict['python_version'] = platform.python_version() + except Exception as ex: + LOG.warning("Unable to determine the python version. " + "Exception is %s.", ex) + + def _update_version_from_version_string(self): + """Update the version from the version string.""" + try: + self.version_dict['openstack_version'] = ( + version.version_info.version_string()) + except Exception as ex: + LOG.warning("Unable to determine the OS version. " + "Exception is %s.", ex) + + def _update_release_from_release_string(self): + """Update the release from the release string.""" + try: + self.version_dict['openstack_release'] = ( + version.version_info.release_string()) + except Exception as ex: + LOG.warning("Unable to get release info. " + "Exception is %s.", ex) + + @staticmethod + def _get_version_info_version(): + """Gets the version. + + :returns: string -- version + """ + return version.version_info.version + + @staticmethod + def _get_version_info_release(): + """Gets the release. + + :returns: string -- release + """ + return version.version_info.release + + def _update_info_from_version_info(self): + """Update class variables from version info.""" + try: + ver = self._get_version_info_version() + if ver: + self.version_dict['openstack_version'] = ver + except Exception as ex: + LOG.warning("Unable to get version info. " + "Exception is %s.", ex) + try: + rel = self._get_version_info_release() + if rel: + self.version_dict['openstack_release'] = rel + except Exception as ex: + LOG.warning("Unable to get release info. " + "Exception is %s.", ex) + + def _update_openstack_info(self): + """Update openstack info.""" + self._update_version_from_version_string() + self._update_release_from_release_string() + self._update_platform() + self._get_python_version() + # Some distributions override with more meaningful information + self._update_info_from_version_info() + + def _update_vmax_info(self, serial_number): + """Update VMAX info. + + :param serial_number: the serial number of the array + """ + vmax_u4v_version_dict = ( + self.rest.get_unisphere_version()) + self.version_dict['unisphere_version'] = ( + vmax_u4v_version_dict['version']) + self.version_dict['serial_number'] = serial_number + vmax_info_dict = self.rest.get_array_serial(serial_number) + self.version_dict['vmax_firmware_version'] = vmax_info_dict['ucode'] + self.version_dict['vmax_model'] = vmax_info_dict['model'] + self.version_dict['vmax_driver_version'] = self.vmax_driver_version + + @debug_required + def gather_version_info(self, serial_number): + """Gather info on the array + + :param serial_number: the serial number of the array + :returns: version_dict + """ + try: + self._update_openstack_info() + self._update_vmax_info(serial_number) + self.print_pretty_table(self.version_dict) + except Exception as ex: + LOG.warning("Unable to gather version info. " + "Exception is %s.", ex) + return self.version_dict + + @debug_required + def gather_volume_info( + self, volume_id, operation, append, **kwargs): + """Gather volume information. + + :param volume_id: the unique volume id key + :param operation: the operation e.g "create" + :param append: append flag + :param kwargs: variable length argument list + :returns: datadict + """ + volume_trace_dict = {} + volume_key_value = {} + datadict = {} + try: + volume_trace_dict = self._fill_volume_trace_dict( + volume_id, operation, append, **kwargs) + volume_trace_dict['volume_updated_time'] = ( + datetime.datetime.fromtimestamp( + int(time.time())).strftime('%Y-%m-%d %H:%M:%S')) + volume_key_value[volume_id] = volume_trace_dict + if not self.volume_trace_list: + self.volume_trace_list.append(volume_key_value.copy()) + else: + self._consolidate_volume_trace_list( + volume_id, volume_trace_dict, volume_key_value) + for datadict in list(self.volume_trace_list): + if volume_id in datadict: + if not append: + self.volume_trace_list.remove(datadict) + return datadict + except Exception as ex: + LOG.warning("Exception in gather volume metadata. " + "Exception is %s.", ex) + return datadict + + def _fill_volume_trace_dict( + self, volume_id, operation, append, **kwargs): + """Populates a dictionary with key value pairs + + :param volume_id: the unique volume id key + :param operation: the operation e.g "create" + :param append: append flag + :param kwargs: variable length argument list + :returns: my_volume_trace_dict + """ + param_dict = locals() + my_volume_trace_dict = {} + for k, v in param_dict.items(): + if self._param_condition(k, v): + my_volume_trace_dict[k] = v + if k == 'kwargs': + for k2, v2 in v.items(): + if self._param_condition(k2, v2): + my_volume_trace_dict[k2] = v2 + elif k2 == 'mv_list' and v2: + for i, item in enumerate(v2, 1): + my_volume_trace_dict["masking_view_%d" % i] = item + elif k2 == 'sg_list' and v2: + for i, item in enumerate(v2, 1): + my_volume_trace_dict["storage_group_%d" % i] = item + + return my_volume_trace_dict + + def _param_condition(self, key, value): + """Determines condition for inclusion. + + :param key: the key + :param value: the value + + :returns: True or False + """ + exclude_list = ('self', 'append', 'mv_list', 'sg_list') + return (value is not None and key not in exclude_list and + not isinstance(value, (dict, + types.FunctionType, + type))) + + @debug_required + def print_pretty_table(self, datadict): + """Prints the data in the dict. + + :param datadict: the data dictionary + """ + t = prettytable.PrettyTable(['Key', 'Value']) + for k, v in datadict.items(): + if v is not None: + t.add_row([k, v]) + + LOG.debug('\n%(output)s\n', {'output': t}) + + def _consolidate_volume_trace_list( + self, volume_id, volume_trace_dict, volume_key_value): + """Consolidate data into self.volume_trace_list + + :param volume_id: the unique volume identifier + :param volume_trace_dict: the existing dict + :param volume_key_value: the volume id key and dict value + """ + is_merged = False + for datadict in list(self.volume_trace_list): + if volume_id in datadict: + for key, dict_value in datadict.items(): + merged_dict = ( + self.utils.merge_dicts( + volume_trace_dict, dict_value)) + self.volume_trace_list.remove(datadict) + volume_key_value[volume_id] = merged_dict + self.volume_trace_list.append(volume_key_value.copy()) + is_merged = True + if not is_merged: + self.volume_trace_list.append(volume_key_value.copy()) + + @debug_required + def update_volume_info_metadata(self, datadict, version_dict): + """Get update volume metadata with volume info + + :param datadict: volume info key value pairs + :param version_dict: version dictionary + :returns: volume_metadata + """ + return self.utils.merge_dicts( + version_dict, *datadict.values()) + + @debug_required + def capture_attach_info( + self, volume, extra_specs, masking_view_dict, host, + is_multipath, is_multiattach): + """Captures attach info in volume metadata + + :param volume: the volume object + :param extra_specs: extra specifications + :param masking_view_dict: masking view dict + :param host: host + :param is_multipath: is mulitipath flag + :param is_multiattach: is multi attach + """ + mv_list, sg_list = [], [] + child_storage_group, parent_storage_group = None, None + initiator_group, port_group = None, None + + if is_multiattach: + operation = 'multi_attach' + mv_list = masking_view_dict['mv_list'] + sg_list = masking_view_dict['sg_list'] + else: + operation = 'attach' + child_storage_group = masking_view_dict[utils.SG_NAME] + parent_storage_group = masking_view_dict[utils.PARENT_SG_NAME] + initiator_group = masking_view_dict[utils.IG_NAME] + port_group = masking_view_dict[utils.PORTGROUPNAME] + + datadict = self.gather_volume_info( + volume.id, operation, False, + serial_number=extra_specs[utils.ARRAY], + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], srp=extra_specs[utils.SRP], + masking_view=masking_view_dict[utils.MV_NAME], + child_storage_group=child_storage_group, + parent_storage_group=parent_storage_group, + initiator_group=initiator_group, + port_group=port_group, + host=host, is_multipath=is_multipath, + identifier_name=self.utils.get_volume_element_name(volume.id), + mv_list=mv_list, sg_list=sg_list) + + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_detach_info( + self, volume, extra_specs, device_id, mv_list, sg_list): + """Captures detach info in volume metadata + + :param volume: the volume object + :param extra_specs: extra specifications + :param device_id: masking view dict + :param mv_list: masking view list + :param sg_list: storage group list + """ + default_sg = self.utils.derive_default_sg_from_extra_specs(extra_specs) + datadict = self.gather_volume_info( + volume.id, 'detach', False, device_id=device_id, + serial_number=extra_specs[utils.ARRAY], + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], srp=extra_specs[utils.SRP], + default_sg_name=default_sg, + identifier_name=self.utils.get_volume_element_name(volume.id), + mv_list=mv_list, sg_list=sg_list + ) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_extend_info( + self, volume, new_size, device_id, extra_specs, array): + """Capture extend info in volume metadata + + :param volume: the volume object + :param new_size: new size + :param device_id: device id + :param extra_specs: extra specifications + :param array: array serial number + """ + default_sg = self.utils.derive_default_sg_from_extra_specs(extra_specs) + datadict = self.gather_volume_info( + volume.id, 'extend', False, volume_size=new_size, + device_id=device_id, + default_sg_name=default_sg, serial_number=array, + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], + srp=extra_specs[utils.SRP], + identifier_name=self.utils.get_volume_element_name(volume.id), + is_compression_disabled=self.utils.is_compression_disabled( + extra_specs)) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_snapshot_info( + self, source, extra_specs, operation, last_ss_name): + """Captures snapshot info in volume metadata + + :param source: the source volume object + :param extra_specs: extra specifications + :param operation: snapshot operation + :param last_ss_name: the last snapshot name + """ + if isinstance(source, volume.Volume): + if 'create' in operation: + snapshot_count = six.text_type(len(source.snapshots)) + else: + snapshot_count = six.text_type(len(source.snapshots) - 1) + default_sg = ( + self.utils.derive_default_sg_from_extra_specs(extra_specs)) + datadict = self.gather_volume_info( + source.id, operation, False, + volume_size=source.size, + default_sg_name=default_sg, + serial_number=extra_specs[utils.ARRAY], + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], + srp=extra_specs[utils.SRP], + identifier_name=( + self.utils.get_volume_element_name(source.id)), + snapshot_count=snapshot_count, + last_ss_name=last_ss_name) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_modify_group( + self, group_name, group_id, add_vols, remove_volumes, array): + """Captures group info after a modify operation + + :param group_name: group name + :param group_id: group id + :param add_vols: add volume list + :param remove_volumes: remove volume list + :param array: array serial number + """ + if not self.version_dict: + self.version_dict = self.gather_version_info(array) + for add_vol in add_vols: + datadict = self.gather_volume_info( + add_vol.id, 'addToGroup', True, + group_name=group_name, group_id=group_id) + add_volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(add_volume_metadata) + + for remove_volume in remove_volumes: + datadict = self.gather_volume_info( + remove_volume.id, 'removeFromGroup', True, + group_name='Removed from %s' % group_name, + group_id='Removed from %s' % group_id) + remove_volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(remove_volume_metadata) + + @debug_required + def capture_create_volume( + self, device_id, volume, group_name, group_id, extra_specs, + rep_info_dict, operation, source_snapshot_id): + """Captures create volume info in volume metadata + + :param device_id: device id + :param volume: volume object + :param group_name: group name + :param group_id: group id + :param extra_specs: additional info + :param rep_info_dict: information gathered from replication + :param operation: the type of create operation + :param source_snapshot_id: the source snapshot id + :returns: volume_metadata dict + """ + rdf_group_no, target_name, remote_array, target_device_id = ( + None, None, None, None) + rep_mode, replication_status, rdf_group_label, use_bias = ( + None, None, None, None) + if rep_info_dict: + rdf_group_no = rep_info_dict['rdf_group_no'] + target_name = rep_info_dict['target_name'] + remote_array = rep_info_dict['remote_array'] + target_device_id = rep_info_dict['target_device_id'] + rep_mode = rep_info_dict['rep_mode'] + replication_status = rep_info_dict['replication_status'] + rdf_group_label = rep_info_dict['rdf_group_label'] + if utils.METROBIAS in extra_specs: + use_bias = extra_specs[utils.METROBIAS] + + default_sg = self.utils.derive_default_sg_from_extra_specs( + extra_specs, rep_mode) + datadict = self.gather_volume_info( + volume.id, operation, True, volume_size=volume.size, + device_id=device_id, + default_sg_name=default_sg, + serial_number=extra_specs[utils.ARRAY], + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], + srp=extra_specs[utils.SRP], + identifier_name=self.utils.get_volume_element_name(volume.id), + source_volid=volume.source_volid, + group_name=group_name, group_id=group_id, + rdf_group_no=rdf_group_no, + target_name=target_name, remote_array=remote_array, + target_device_id=target_device_id, + source_snapshot_id=source_snapshot_id, + rep_mode=rep_mode, replication_status=replication_status, + rdf_group_label=rdf_group_label, use_bias=use_bias, + is_compression_disabled = ( + 'yes' if self.utils.is_compression_disabled( + extra_specs) else 'no') + ) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def gather_replication_info( + self, rdf_group_no=None, + target_name=None, remote_array=None, + target_device_id=None, replication_status=None, + rep_mode=None, rdf_group_label=None): + """Gathers replication information + + :param rdf_group_no: RDF group number + :param target_name: target volume name + :param remote_array: remote array serialnumber + :param target_device_id: target device id + :param replication_status: replication status + :param rep_mode: replication mode, sync, async, metro + :param rdf_group_label: rdf group label + :returns: rep_dict + """ + param_dict = locals() + return self._fill_volume_trace_dict(param_dict) + + @debug_required + def capture_failover_volume( + self, volume, target_device, remote_array, rdf_group, device_id, + array, extra_specs, failover, vol_grp_name, + replication_status, rep_mode): + """Captures failover info in volume metadata + + :param volume: volume object + :param target_device: the device to failover to + :param remote_array: the array to failover to + :param rdf_group: the rdf group + :param device_id: the device to failover from + :param array: the array to failover from + :param extra_specs: additional info + :param failover: failover flag + :param vol_grp_name: async group name + :param replication_status: volume replication status + :param rep_mode: replication mode + """ + operation = "Failover" if failover else "Failback" + datadict = self.gather_volume_info( + volume.id, operation, True, volume_size=volume.size, + device_id=target_device, + serial_number=remote_array, + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], + srp=extra_specs[utils.SRP], + identifier_name=self.utils.get_volume_element_name(volume.id), + source_volid=volume.source_volid, + rdf_group_no=rdf_group, remote_array=array, + target_device_id=device_id, vol_grp_name=vol_grp_name, + replication_status=replication_status, rep_mode=rep_mode + ) + + self.version_dict = ( + self.gather_version_info(remote_array)) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_manage_existing( + self, volume, rep_info_dict, device_id, extra_specs): + """Captures manage existing info in volume metadata + + :param volume: volume object + :param rep_info_dict: information gathered from replication + :param device_id: the VMAX device id + :param extra_specs: the extra specs + """ + operation = "manage_existing_volume" + rdf_group_no, target_name, remote_array, target_device_id = ( + None, None, None, None) + rep_mode, replication_status, rdf_group_label = ( + None, None, None) + if rep_info_dict: + rdf_group_no = rep_info_dict['rdf_group_no'] + target_name = rep_info_dict['target_name'] + remote_array = rep_info_dict['remote_array'] + target_device_id = rep_info_dict['target_device_id'] + rep_mode = rep_info_dict['rep_mode'] + replication_status = rep_info_dict['replication_status'] + rdf_group_label = rep_info_dict['rdf_group_label'] + + default_sg = self.utils.derive_default_sg_from_extra_specs( + extra_specs, rep_mode) + datadict = self.gather_volume_info( + volume.id, operation, True, volume_size=volume.size, + device_id=device_id, + default_sg_name=default_sg, + serial_number=extra_specs[utils.ARRAY], + service_level=extra_specs[utils.SLO], + workload=extra_specs[utils.WORKLOAD], + srp=extra_specs[utils.SRP], + identifier_name=self.utils.get_volume_element_name(volume.id), + source_volid=volume.source_volid, + rdf_group_no=rdf_group_no, + target_name=target_name, remote_array=remote_array, + target_device_id=target_device_id, + rep_mode=rep_mode, replication_status=replication_status, + rdf_group_label=rdf_group_label + ) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) + + @debug_required + def capture_retype_info( + self, volume_id, volume_size, device_id, array, srp, target_slo, + target_workload, target_sg_name, is_rep_enabled, rep_mode, + is_compression_disabled): + """Captures manage existing info in volume metadata + + :param volume_id: volume identifier + :param volume_size: volume size + :param device_id: the VMAX device id + :param array: the VMAX serialnumber + :param srp: VMAX SRP + :param target_slo: volume name + :param target_workload: the VMAX device id + :param is_rep_enabled: replication enabled flag + :param rep_mode: replication mode + :param is_compression_disabled: compression disabled flag + """ + operation = "retype" + datadict = self.gather_volume_info( + volume_id, operation, False, volume_size=volume_size, + device_id=device_id, + target_sg_name=target_sg_name, + serial_number=array, + target_service_level=target_slo, + target_workload=target_workload, + srp=srp, + identifier_name=self.utils.get_volume_element_name(volume_id), + is_rep_enabled=('yes' if is_rep_enabled else 'no'), + rep_mode=rep_mode, is_compression_disabled = ( + 'yes' if is_compression_disabled else 'no') + ) + volume_metadata = self.update_volume_info_metadata( + datadict, self.version_dict) + self.print_pretty_table(volume_metadata) diff --git a/cinder/volume/drivers/dell_emc/vmax/rest.py b/cinder/volume/drivers/dell_emc/vmax/rest.py index 0586975f459..32206d2c13f 100644 --- a/cinder/volume/drivers/dell_emc/vmax/rest.py +++ b/cinder/volume/drivers/dell_emc/vmax/rest.py @@ -430,14 +430,24 @@ class VMAXRest(object): :return: version and major_version(e.g. ("V8.4.0.16", "84")) """ version, major_version = None, None - target_uri = "/%s/system/version" % U4V_VERSION - response = self._get_request(target_uri, 'version') + response = self.get_unisphere_version() if response and response.get('version'): version = response['version'] version_list = version.split('.') major_version = version_list[0][1] + version_list[1] return version, major_version + def get_unisphere_version(self): + """Get the unisphere version from the server. + + :returns: version dict + """ + version_url = "/%s/system/version" % U4V_VERSION + version_dict = self._get_request(version_url, 'version') + if not version_dict: + LOG.error("Unisphere version info not found.") + return version_dict + def get_srp_by_name(self, array, srp=None): """Returns the details of a storage pool. diff --git a/cinder/volume/drivers/dell_emc/vmax/utils.py b/cinder/volume/drivers/dell_emc/vmax/utils.py index 48f74f48e3e..af471d7d48d 100644 --- a/cinder/volume/drivers/dell_emc/vmax/utils.py +++ b/cinder/volume/drivers/dell_emc/vmax/utils.py @@ -690,6 +690,36 @@ class VMAXUtils(object): return True return False + def derive_default_sg_from_extra_specs(self, extra_specs, rep_mode=None): + """Get the name of the default sg from the extra specs. + + :param extra_specs: extra specs + :returns: default sg - string + """ + do_disable_compression = self.is_compression_disabled( + extra_specs) + rep_enabled = self.is_replication_enabled(extra_specs) + return self.get_default_storage_group_name( + extra_specs[SRP], extra_specs[SLO], + extra_specs[WORKLOAD], + is_compression_disabled=do_disable_compression, + is_re=rep_enabled, rep_mode=rep_mode) + + @staticmethod + def merge_dicts(d1, *args): + """Merge dictionaries + + :param d1: dict 1 + :param *args: one or more dicts + :returns: merged dict + """ + d2 = {} + for d in args: + d2 = d.copy() + d2.update(d1) + d1 = d2 + return d2 + @staticmethod def get_temp_failover_grp_name(rep_config): """Get the temporary group name used for failover. diff --git a/releasenotes/notes/vmax-metadata-ac9bdd31e7e561c3.yaml b/releasenotes/notes/vmax-metadata-ac9bdd31e7e561c3.yaml new file mode 100644 index 00000000000..6936ab578f6 --- /dev/null +++ b/releasenotes/notes/vmax-metadata-ac9bdd31e7e561c3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Log VMAX specific metadata of a volume if debug is enabled.