diff --git a/cinder/tests/unit/test_nimble.py b/cinder/tests/unit/test_nimble.py index 3b8551eff..5786d8a8e 100644 --- a/cinder/tests/unit/test_nimble.py +++ b/cinder/tests/unit/test_nimble.py @@ -17,10 +17,13 @@ import sys import mock from oslo_config import cfg +from oslo_utils import units +from cinder import context from cinder import exception from cinder.objects import volume as obj_volume from cinder import test +from cinder.tests.unit import fake_constants as fake from cinder.volume.drivers import nimble from cinder.volume import volume_types @@ -29,6 +32,8 @@ CONF = cfg.CONF NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.client' NIMBLE_URLLIB2 = 'six.moves.urllib.request' NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random' +NIMBLE_ISCSI_DRIVER = 'cinder.volume.drivers.nimble.NimbleISCSIDriver' +DRIVER_VERSION = '2.0.3' FAKE_ENUM_STRING = """ @@ -117,11 +122,29 @@ FAKE_GET_VOL_INFO_RESPONSE = { 'agent-type': 1, 'online': False}} +FAKE_GET_VOL_INFO_BACKUP_RESPONSE = { + 'err-list': {'err-list': [{'code': 0}]}, + 'vol': {'target-name': 'iqn.test', + 'name': 'test_vol', + 'agent-type': 1, + 'clone': 1, + 'base-snap': 'test-backup-snap', + 'parent-vol': 'volume-' + fake.volume2_id, + 'online': False}} + +FAKE_GET_SNAP_INFO_BACKUP_RESPONSE = { + 'err-list': {'err-list': [{'code': 0}]}, + 'snap': {'description': "backup-vol-" + fake.volume2_id, + 'name': 'test-backup-snap', + 'vol': 'volume-' + fake.volume_id} +} + FAKE_GET_VOL_INFO_ONLINE = { 'err-list': {'err-list': [{'code': 0}]}, 'vol': {'target-name': 'iqn.test', 'name': 'test_vol', 'agent-type': 1, + 'size': int(1.75 * units.Gi), 'online': True}} FAKE_GET_VOL_INFO_ERROR = { @@ -134,8 +157,7 @@ FAKE_GET_VOL_INFO_RESPONSE_WITH_SET_AGENT_TYPE = { 'name': 'test_vol', 'agent-type': 5}} - -FAKE_TYPE_ID = 12345 +FAKE_TYPE_ID = fake.volume_type_id def create_configuration(username, password, ip_address, @@ -475,6 +497,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + @mock.patch(NIMBLE_ISCSI_DRIVER + ".is_volume_backup_clone", mock.Mock( + return_value = ['', ''])) def test_delete_volume(self): self.mock_client_service.service.onlineVol.return_value = \ FAKE_GENERIC_POSITIVE_RESPONSE @@ -492,6 +516,46 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): request={'name': 'testvolume', 'sid': 'a9b9aba7'})] self.mock_client_service.assert_has_calls(expected_calls) + @mock.patch(NIMBLE_URLLIB2) + @mock.patch(NIMBLE_CLIENT) + @mock.patch.object(obj_volume.VolumeList, 'get_all', + mock.Mock(return_value=[])) + @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( + 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + @mock.patch(NIMBLE_ISCSI_DRIVER + ".is_volume_backup_clone", mock.Mock( + return_value=['test-backup-snap', 'volume-' + fake.volume_id])) + def test_delete_volume_with_backup(self): + self.mock_client_service.service.onlineVol.return_value = \ + FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.service.deleteVol.return_value = \ + FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.service.dissocProtPol.return_value = \ + FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.service.onlineSnap.return_value = \ + FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.service.deleteSnap.return_value = \ + FAKE_GENERIC_POSITIVE_RESPONSE + + self.driver.delete_volume({'name': 'testvolume'}) + expected_calls = [mock.call.service.onlineVol( + request={ + 'online': False, 'name': 'testvolume', 'sid': 'a9b9aba7'}), + mock.call.service.dissocProtPol( + request={'vol-name': 'testvolume', 'sid': 'a9b9aba7'}), + mock.call.service.deleteVol( + request={'name': 'testvolume', 'sid': 'a9b9aba7'}), + mock.call.service.onlineSnap( + request={'vol': 'volume-' + fake.volume_id, + 'name': 'test-backup-snap', + 'online': False, + 'sid': 'a9b9aba7'}), + mock.call.service.deleteSnap( + request={'vol': 'volume-' + fake.volume_id, + 'name': 'test-backup-snap', + 'sid': 'a9b9aba7'})] + + self.mock_client_service.assert_has_calls(expected_calls) + @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @mock.patch.object(obj_volume.VolumeList, 'get_all', @@ -517,15 +581,18 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @mock.patch.object(obj_volume.VolumeList, 'get_all', mock.Mock(return_value=[])) @mock.patch.object(volume_types, 'get_volume_type_extra_specs', - mock.Mock(type_id=FAKE_TYPE_ID, return_value={ - 'nimble:perfpol-name': 'default', - 'nimble:encryption': 'yes', - 'nimble:multi-initiator': 'false'})) + mock.Mock(type_id=FAKE_TYPE_ID, + return_value= + {'nimble:perfpol-name': 'default', + 'nimble:encryption': 'yes', + 'nimble:multi-initiator': 'false'})) @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*', False)) + @mock.patch.object(obj_volume.VolumeList, 'get_all') @mock.patch(NIMBLE_RANDOM) - def test_create_cloned_volume(self, mock_random): - mock_random.sample.return_value = 'abcdefghijkl' + def test_create_cloned_volume(self, mock_random, mock_volume_list): + mock_random.sample.return_value = fake.volume_id + mock_volume_list.return_value = [] self.mock_client_service.service.snapVol.return_value = \ FAKE_GENERIC_POSITIVE_RESPONSE self.mock_client_service.service.cloneVol.return_value = \ @@ -534,25 +601,36 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): FAKE_GET_VOL_INFO_RESPONSE self.mock_client_service.service.getNetConfig.return_value = \ FAKE_POSITIVE_NETCONFIG_RESPONSE + + volume = obj_volume.Volume(context.get_admin_context(), + id=fake.volume_id, + size=5.0, + _name_id=None, + display_name='', + volume_type_id=FAKE_TYPE_ID + ) + src_volume = obj_volume.Volume(context.get_admin_context(), + id=fake.volume2_id, + _name_id=None, + size=5.0) self.assertEqual({ 'provider_location': '172.18.108.21:3260 iqn.test 0', 'provider_auth': None}, - self.driver.create_cloned_volume({'name': 'volume', - 'size': 5, - 'volume_type_id': FAKE_TYPE_ID}, - {'name': 'testvolume', - 'size': 5})) + self.driver.create_cloned_volume(volume, src_volume)) expected_calls = [mock.call.service.snapVol( request={ - 'vol': 'testvolume', - 'snapAttr': {'name': 'openstack-clone-volume-abcdefghijkl', + 'vol': "volume-" + fake.volume2_id, + 'snapAttr': {'name': 'openstack-clone-volume-' + + fake.volume_id + + "-" + fake.volume_id, 'description': ''}, 'sid': 'a9b9aba7'}), mock.call.service.cloneVol( request={ - 'snap-name': 'openstack-clone-volume-abcdefghijkl', + 'snap-name': 'openstack-clone-volume-' + fake.volume_id + + "-" + fake.volume_id, 'attr': {'snap-quota': sys.maxsize, - 'name': 'volume', + 'name': 'volume-' + fake.volume_id, 'quota': 5368709120, 'reserve': 5368709120, 'online': True, @@ -561,7 +639,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): 'multi-initiator': 'false', 'perfpol-name': 'default', 'agent-type': 5}, - 'name': 'testvolume', + 'name': 'volume-' + fake.volume2_id, 'sid': 'a9b9aba7'})] self.mock_client_service.assert_has_calls(expected_calls) @@ -618,6 +696,21 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): {'name': 'volume-abcdef'}, {'source-name': 'test-vol'}) + @mock.patch(NIMBLE_URLLIB2) + @mock.patch(NIMBLE_CLIENT) + @mock.patch.object(obj_volume.VolumeList, 'get_all', + mock.Mock(return_value=[])) + @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( + 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + def test_manage_volume_get_size(self): + self.mock_client_service.service.getNetConfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) + self.mock_client_service.service.getVolInfo.return_value = ( + FAKE_GET_VOL_INFO_ONLINE) + size = self.driver.manage_existing_get_size( + {'name': 'volume-abcdef'}, {'source-name': 'test-vol'}) + self.assertEqual(1, size) + @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @mock.patch.object(obj_volume.VolumeList, 'get_all', @@ -726,7 +819,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): def test_get_volume_stats(self): self.mock_client_service.service.getGroupConfig.return_value = \ FAKE_POSITIVE_GROUP_CONFIG_RESPONSE - expected_res = {'driver_version': '2.0.2', + expected_res = {'driver_version': DRIVER_VERSION, 'vendor_name': 'Nimble', 'volume_backend_name': 'NIMBLE', 'storage_protocol': 'iSCSI', @@ -739,6 +832,33 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): expected_res, self.driver.get_volume_stats(refresh=True)) + @mock.patch(NIMBLE_URLLIB2) + @mock.patch(NIMBLE_CLIENT) + @mock.patch.object(obj_volume.VolumeList, 'get_all', + mock.Mock(return_value=[])) + @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( + 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + def test_is_volume_backup_clone(self): + self.mock_client_service.service.getVolInfo.return_value = \ + FAKE_GET_VOL_INFO_BACKUP_RESPONSE + self.mock_client_service.service.getSnapInfo.return_value = \ + FAKE_GET_SNAP_INFO_BACKUP_RESPONSE + volume = obj_volume.Volume(context.get_admin_context(), + id=fake.volume_id, + _name_id=None) + self.assertEqual(("test-backup-snap", "volume-" + fake.volume_id), + self.driver.is_volume_backup_clone(volume)) + expected_calls = [ + mock.call.service.getVolInfo( + request={'name': 'volume-' + fake.volume_id, + 'sid': 'a9b9aba7'}), + mock.call.service.getSnapInfo( + request={'sid': 'a9b9aba7', + 'vol': 'volume-' + fake.volume2_id, + 'name': 'test-backup-snap'}) + ] + self.mock_client_service.assert_has_calls(expected_calls) + class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase): @@ -862,7 +982,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): expected_res = { 'driver_volume_type': 'iscsi', 'data': { - 'target_lun': '14', + 'target_lun': 14, 'volume_id': 12, 'target_iqn': '13', 'target_discovered': False, @@ -905,7 +1025,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): expected_res = { 'driver_volume_type': 'iscsi', 'data': { - 'target_lun': '14', + 'target_lun': 14, 'volume_id': 12, 'target_iqn': '13', 'target_discovered': False, diff --git a/cinder/volume/drivers/nimble.py b/cinder/volume/drivers/nimble.py index 801e87306..cd9ea10eb 100644 --- a/cinder/volume/drivers/nimble.py +++ b/cinder/volume/drivers/nimble.py @@ -22,6 +22,7 @@ import functools import random import re import six +import ssl import string import sys @@ -38,7 +39,7 @@ from cinder.volume.drivers.san import san from cinder.volume import volume_types -DRIVER_VERSION = '2.0.2' +DRIVER_VERSION = '2.0.3' AES_256_XTS_CIPHER = 2 DEFAULT_CIPHER = 3 EXTRA_SPEC_ENCRYPTION = 'nimble:encryption' @@ -61,6 +62,12 @@ SM_SUBNET_MGMT_PLUS_DATA = 4 LUN_ID = '0' WARN_LEVEL = 0.8 +# Work around for ubuntu_openssl_bug_965371. Python soap client suds +# throws the error ssl-certificate-verify-failed-error, workaround to disable +# ssl check for now +if hasattr(ssl, '_create_unverified_context'): + ssl._create_default_https_context = ssl._create_unverified_context + LOG = logging.getLogger(__name__) nimble_opts = [ @@ -97,6 +104,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver): Added Manage/Unmanage volume support 2.0.1 - Added multi-initiator support through extra-specs 2.0.2 - Fixed supporting extra specs while cloning vols + 2.0.3 - Support for Force Backup """ VERSION = DRIVER_VERSION @@ -211,14 +219,57 @@ class NimbleISCSIDriver(san.SanISCSIDriver): self.configuration.nimble_pool_name, reserve) return self._get_model_info(volume['name']) + def is_volume_backup_clone(self, volume): + """Check if the volume is created through cinder-backup workflow. + + :param volume: reference to volume from delete_volume() + """ + vol_info = self.APIExecutor.get_vol_info(volume.name) + if vol_info['clone'] and vol_info['base-snap'] and vol_info[ + 'parent-vol']: + LOG.debug("Nimble base-snap exists for volume :%s", volume['name']) + volume_name_prefix = volume.name.replace(volume.id, "") + LOG.debug("volume_name_prefix : %s", volume_name_prefix) + snap_info = self.APIExecutor.get_snap_info(vol_info['base-snap'], + vol_info['parent-vol']) + if snap_info['description'] and "backup-vol-" in snap_info[ + 'description']: + parent_vol_id = vol_info['parent-vol' + ].replace(volume_name_prefix, "") + if "backup-vol-" + parent_vol_id in snap_info['description']: + LOG.info(_LI("nimble backup-snapshot exists name: %s"), + snap_info['name']) + return snap_info['name'], snap_info['vol'] + return "", "" + def delete_volume(self, volume): """Delete the specified volume.""" + snap_name, vol_name = self.is_volume_backup_clone(volume) self.APIExecutor.online_vol(volume['name'], False, ignore_list=['SM-enoent']) self.APIExecutor.dissociate_volcoll(volume['name'], ignore_list=['SM-enoent']) self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent']) + # Nimble backend does not delete the snapshot from the parent volume + # if there is a dependent clone. So the deletes need to be in reverse + # order i.e. + # 1. First delete the clone volume used for backup + # 2. Delete the base snapshot used for clone from the parent volume. + # This is only done for the force backup clone operation as it is + # a temporary operation in which we are certain that the snapshot does + # not need to be preserved after the backup is completed. + + if snap_name and vol_name: + self.APIExecutor.online_snap(vol_name, + False, + snap_name, + ignore_list=['SM-ealready', + 'SM-enoent']) + self.APIExecutor.delete_snap(vol_name, + snap_name, + ignore_list=['SM-enoent']) + def _generate_random_string(self, length): """Generates random_string.""" char_set = string.ascii_lowercase @@ -252,7 +303,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver): snapshot = {'volume_name': src_vref['name'], 'name': snapshot_name, 'volume_size': src_vref['size'], - 'display_name': '', + 'display_name': volume.display_name, 'display_description': ''} self.APIExecutor.snap_vol(snapshot) self._clone_volume_from_snapshot(volume, snapshot) @@ -479,7 +530,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver): properties['target_discovered'] = False # whether discovery was used properties['target_portal'] = iscsi_portal properties['target_iqn'] = iqn - properties['target_lun'] = lun_num + properties['target_lun'] = int(lun_num) properties['volume_id'] = volume['id'] # used by xen currently return { 'driver_volume_type': 'iscsi', @@ -750,6 +801,31 @@ class NimbleAPIExecutor(object): vol_name) return response['vol'] + @_connection_checker + @_response_checker + def _execute_get_snap_info(self, snap_name, vol_name): + LOG.info(_LI('Getting snapshot information for %(vol_name)s ' + '%(snap_name)s'), {'vol_name': vol_name, + 'snap_name': snap_name}) + return self.client.service.getSnapInfo(request={'sid': self.sid, + 'vol': vol_name, + 'name': snap_name}) + + def get_snap_info(self, snap_name, vol_name): + """Get snapshot information. + + :param snap_name: snapshot name + :param vol_name: volume name + :return: response object + """ + + response = self._execute_get_snap_info(snap_name, vol_name) + LOG.info(_LI('Successfully got snapshot information for snapshot ' + '%(snap)s and %(volume)s'), + {'snap': snap_name, + 'volume': vol_name}) + return response['snap'] + @_connection_checker @_response_checker def online_vol(self, vol_name, online_flag, *args, **kwargs): @@ -795,10 +871,12 @@ class NimbleAPIExecutor(object): volume_name = snapshot['volume_name'] snap_name = snapshot['name'] # Set snapshot description - display_list = [getattr(snapshot, 'display_name', ''), + display_list = [getattr(snapshot, 'display_name', snapshot[ + 'display_name']), getattr(snapshot, 'display_description', '')] snap_description = ':'.join(filter(None, display_list)) # Limit to 254 characters + LOG.debug("snap_description %s", snap_description) snap_description = snap_description[:254] LOG.info(_LI('Creating snapshot for volume_name=%(vol)s' ' snap_name=%(name)s snap_description=%(desc)s'), diff --git a/releasenotes/notes/nimble-add-force-backup-539e1e5c72f84e61.yaml b/releasenotes/notes/nimble-add-force-backup-539e1e5c72f84e61.yaml new file mode 100644 index 000000000..584c04300 --- /dev/null +++ b/releasenotes/notes/nimble-add-force-backup-539e1e5c72f84e61.yaml @@ -0,0 +1,4 @@ +--- +features: + - Support Force backup of in-use cinder volumes + for Nimble Storage.