diff --git a/cinder/tests/unit/volume/drivers/ibm/test_storwize_svc.py b/cinder/tests/unit/volume/drivers/ibm/test_storwize_svc.py index 1e8c8b8f885..982d30a65e7 100644 --- a/cinder/tests/unit/volume/drivers/ibm/test_storwize_svc.py +++ b/cinder/tests/unit/volume/drivers/ibm/test_storwize_svc.py @@ -82,6 +82,7 @@ class StorwizeSVCManagementSimulator(object): self._partnershipcandidate_list = {} self._rcconsistgrp_list = {} self._volumegroup_list = {} + self._volumegroup_snapshot_list = {} self._system_list = {'storwize-svc-sim': {'id': '0123456789ABCDEF', 'name': 'storwize-svc-sim'}, 'aux-svc-sim': {'id': 'ABCDEF0123456789', @@ -377,7 +378,8 @@ class StorwizeSVCManagementSimulator(object): 'removehostmappings', 'removefcmaps', 'removercrelationships', - 'novolumegroup' + 'novolumegroup', + 'ignorelegacy' ] one_param_args = [ 'chapsecret', @@ -418,7 +420,8 @@ class StorwizeSVCManagementSimulator(object): 'pool', 'site', 'buffersize', - 'volumegroup' + 'volumegroup', + 'snapshot' ] no_or_one_param_args = [ 'autoexpand', @@ -2652,6 +2655,108 @@ port_speed!N/A del self._volumegroup_list[volumegroup_name] return ('', '') + def _cmd_addsnapshot(self, **kwargs): + # Create a Volumegroup snapshot + volumegroup_snapshot_info = {} + volumegroup_snapshot_info['id'] = self._find_unused_id( + self._volumegroup_snapshot_list) + volumegroup_name = kwargs['volumegroup'] + if 'name' in kwargs: + volumegroup_snapshot_info['name'] = kwargs["name"].strip('\'\"') + else: + volumegroup_snapshot_info['name'] = ( + 'vg_snap-' + volumegroup_snapshot_info['id']) + volumegroup_snapshot_info['volume_group_id'] = ( + self._volumegroup_list[volumegroup_name]['id']) + volumegroup_snapshot_info['volume_group_name'] = volumegroup_name + volumegroup_snapshot_info['time'] = '' + volumegroup_snapshot_info['state'] = 'active' + volumegroup_snapshot_info['matches_group'] = 'yes' + volumegroup_snapshot_info['parent_uid'] = ( + self._volumegroup_list[volumegroup_name]['uid']) + volumegroup_snapshot_info['expiration_time'] = '' + volumegroup_snapshot_info['protection_provisioned_capacity'] = '1.00GB' + volumegroup_snapshot_info['protection_written_capacity'] = '0.75MB' + volumegroup_snapshot_info['operation_start_time'] = '' + volumegroup_snapshot_info['operation_completion_estimate'] = '' + volumegroup_snapshot_info['owner_id'] = '' + volumegroup_snapshot_info['owner_name'] = '' + volumegroup_snapshot_info['auto_snapshot'] = 'no' + self._volumegroup_snapshot_list[volumegroup_snapshot_info['name']] = ( + volumegroup_snapshot_info) + return ('Snapshot, id [' + volumegroup_snapshot_info['id'] + + '], successfully created or triggered', '') + + def _cmd_lsvolumegroupsnapshot(self, **kwargs): + # List the volume group snapshot + rows = [] + rows.append(['id', 'name', 'volume_group_id', 'volume_group_name', + 'time', 'state', 'matches_group', 'parent_uid', + 'expiration_time', 'protection_provisioned_capacity', + 'protection_written_capacity', 'operation_start_time', + 'operation_completion_estimate', 'owner_id', + 'owner_name', 'auto_snapshot']) + if 'snapshot' and 'volumegroup' not in kwargs: + found = False + for volumegroup_snapshot in sorted( + self._volumegroup_snapshot_list.keys()): + volumegroup_snapshot_info = self._volumegroup_snapshot_list[ + volumegroup_snapshot] + if 'filtervalue' not in kwargs: + rows.append( + [volumegroup_snapshot_info['id'], + volumegroup_snapshot_info['name'], + volumegroup_snapshot_info['volume_group_id'], + volumegroup_snapshot_info['volume_group_name'], + '', 'active', 'yes', + volumegroup_snapshot_info['parent_uid'], '', '1.00GB', + '0.75MB', '', '', '', '', 'no']) + found = True + if found: + return self._print_info_cmd(rows=rows, **kwargs) + else: + return ('', '') + else: + volumegroup_snapshot_info = kwargs['snapshot'].strip('\'\"') + if volumegroup_snapshot_info not in ( + self._volumegroup_snapshot_list): + return self._errors['CMMVC5804E'] + volumegroup_snapshot_info = self._volumegroup_snapshot_list[ + volumegroup_snapshot_info] + rows.append( + [volumegroup_snapshot_info['id'], + volumegroup_snapshot_info['name'], + volumegroup_snapshot_info['volume_group_id'], + volumegroup_snapshot_info['volume_group_name'], + volumegroup_snapshot_info['time'], + volumegroup_snapshot_info['state'], + volumegroup_snapshot_info['matches_group'], + volumegroup_snapshot_info['parent_uid'], + volumegroup_snapshot_info['expiration_time'], + volumegroup_snapshot_info['protection_provisioned_capacity'], + volumegroup_snapshot_info['protection_written_capacity'], + volumegroup_snapshot_info['operation_start_time'], + volumegroup_snapshot_info['operation_completion_estimate'], + volumegroup_snapshot_info['owner_id'], + volumegroup_snapshot_info['owner_name'], + volumegroup_snapshot_info['auto_snapshot']]) + + if 'delim' in kwargs: + for index in range(len(rows)): + rows[index] = kwargs['delim'].join(rows[index]) + return ('%s' % '\n'.join(rows), '') + + def _cmd_rmsnapshot(self, **kwargs): + # Delete a Volume Group snapshot + if 'snapshot' and 'volumegroup' not in kwargs: + return self._errors['CMMVC5701E'] + + volumegroup_snapshot_name = kwargs['snapshot'].strip('\'\"') + if volumegroup_snapshot_name not in self._volumegroup_snapshot_list: + return self._errors['CMMVC9755E'] + del self._volumegroup_snapshot_list[volumegroup_snapshot_name] + return ('', '') + def _cmd_mkrcconsistgrp(self, **kwargs): master_sys = self._system_list['storwize-svc-sim'] aux_sys = self._system_list['aux-svc-sim'] @@ -5563,6 +5668,31 @@ class StorwizeSVCCommonDriverTestCase(test.TestCase): "CG created failed") return grp + def _create_volumegroup_type_and_volumegroup(self, vol_type_ref, + is_pool=None, + is_io_grp=None): + # Create volumegroup type + volumegroup_spec = {'volume_group_enabled': ' True'} + if is_pool: + volumegroup_spec.update({'volume_group_pool': is_pool}) + if is_io_grp: + volumegroup_spec.update({'volume_group_iogrp': is_io_grp}) + + volumegroup_type_ref = group_types.create(self.ctxt, + 'volumegroup_type', + volumegroup_spec) + volumegroup_type = objects.GroupType.get_by_id( + self.ctxt, volumegroup_type_ref['id']) + + # Create volumegroup + volumegroup = testutils.create_group( + self.ctxt, group_type_id=volumegroup_type.id, + volume_type_ids=[vol_type_ref['id']]) + + model_update = self.driver.create_group(self.ctxt, volumegroup) + return (volumegroup_type_ref, volumegroup_type, + volumegroup, model_update) + def _create_group_snapshot_in_db(self, group_id, **kwargs): group_snapshot = testutils.create_group_snapshot(self.ctxt, group_id=group_id, @@ -7206,6 +7336,139 @@ class StorwizeSVCCommonDriverTestCase(test.TestCase): self.assertEqual(fields.GroupStatus.DELETED, model_update[0]['status']) + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'get_system_info') + def test_storwize_create_and_delete_volumegroup_snapshot( + self, get_system_info): + """Test creation and deletion of volumegroup snapshot""" + fake_system_info = {'code_level': (8, 5, 1, 0), + 'system_name': 'storwize-svc-sim', + 'system_id': '0123456789ABCDEF'} + get_system_info.return_value = fake_system_info + self.driver.do_setup(None) + + # Create volume + vol_type_ref = volume_types.create(self.ctxt, 'non_rep_type', {}) + vol_type = objects.VolumeType.get_by_id(self.ctxt, + vol_type_ref['id']) + volume = self._generate_vol_info(vol_type) + self.driver.create_volume(volume) + + # Create volumegroup type and volumegroup + (volumegroup_type_ref, volumegroup_type, volumegroup, + model_update) = self._create_volumegroup_type_and_volumegroup( + vol_type_ref) + self.assertEqual(fields.GroupStatus.AVAILABLE, + model_update['status']) + + # Add volumes to volumegroup + add_vols = [volume] + remove_vols = [volume] + (model_update, add_volumes_update, remove_volumes_update) = ( + self.driver.update_group(self.ctxt, volumegroup, add_vols, [])) + + self.assertEqual(fields.GroupStatus.AVAILABLE, + model_update['status']) + + # Create group-snapshot + group_snapshot, snapshots = self._create_group_snapshot_in_db( + volumegroup.id, group_type_id=volumegroup_type_ref.id) + + model_update, snapshots_model = ( + self.driver.create_group_snapshot(self.ctxt, group_snapshot, + snapshots)) + self.assertEqual(fields.GroupSnapshotStatus.AVAILABLE, + model_update['status']) + + for snapshot in snapshots_model: + self.assertEqual(fields.SnapshotStatus.AVAILABLE, + snapshot['status']) + + # Validating the snapshot_name property value + # from metadata of the snapshot + groupsnapshot_name = self.driver._get_volumegroup_snapshot_name( + group_snapshot) + for snapshot in snapshots: + self.assertEqual(groupsnapshot_name, + snapshot.metadata['snapshot_name']) + + # Delete group-snapshot + model_update, snapshots_model = self.driver.delete_group_snapshot( + self.ctxt, group_snapshot, snapshots) + self.assertEqual(fields.GroupSnapshotStatus.DELETED, + model_update['status']) + + for snapshot in snapshots_model: + self.assertEqual(fields.SnapshotStatus.DELETED, + snapshot['status']) + + # Remove the volumes from volumegroup + (model_update, add_volumes_update, + remove_volumes_update) = self.driver.update_group( + self.ctxt, volumegroup, [], remove_vols) + + self.assertEqual(fields.GroupStatus.AVAILABLE, + model_update['status']) + + # Delete Volume Group + model_update = self.driver.delete_group(self.ctxt, volumegroup, + []) + self.assertEqual(fields.GroupStatus.DELETED, + model_update[0]['status']) + + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'get_system_info') + @mock.patch.object(cinder.volume.volume_utils, + 'is_group_a_type') + @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'create_volumegroup_snapshot') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'delete_volumegroup_snapshot') + def test_storwize_create_and_delete_volumegroup_snapshot_calls( + self, delete_volumegroup_snapshot, + create_volumegroup_snapshot, is_grp_a_cg_snapshot_type, + vg_type, get_system_info): + """Test creation and deletion of volumegroup snapshot""" + fake_system_info = {'code_level': (8, 5, 1, 0), + 'system_name': 'storwize-svc-sim', + 'system_id': '0123456789ABCDEF'} + get_system_info.return_value = fake_system_info + self.driver.do_setup(None) + + # Mocking volume-group-enabled spec as true + is_grp_a_cg_snapshot_type.side_effect = [False, False, False] + vg_type.side_effect = [False, False, True, False, True, + False, False, True, False, False, True] + + # Create volume group + type_ref = volume_types.create(self.ctxt, 'testtype', None) + group = testutils.create_group(self.ctxt, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[type_ref['id']]) + + # Create volume group snapshot + group_snapshot, snapshots = self._create_group_snapshot_in_db( + group.id) + + model_update, snapshots_model = ( + self.driver.create_group_snapshot(self.ctxt, group_snapshot, + snapshots)) + self.assertEqual(fields.GroupSnapshotStatus.AVAILABLE, + model_update['status']) + self.assertTrue(create_volumegroup_snapshot.called) + + # Delete volume group snapshot + model_update, snapshots_model = ( + self.driver.delete_group_snapshot(self.ctxt, group_snapshot, + snapshots)) + self.assertEqual(fields.GroupSnapshotStatus.DELETED, + model_update['status']) + self.assertTrue(delete_volumegroup_snapshot.called) + + # Delete volume group + self.driver.delete_group(self.ctxt, group, []) + @mock.patch.object(storwize_svc_common.StorwizeHelpers, 'create_rccg') def test_storwize_group_create(self, create_rccg): diff --git a/cinder/volume/drivers/ibm/storwize_svc/storwize_const.py b/cinder/volume/drivers/ibm/storwize_svc/storwize_const.py index ef841732c81..21953321598 100644 --- a/cinder/volume/drivers/ibm/storwize_svc/storwize_const.py +++ b/cinder/volume/drivers/ibm/storwize_svc/storwize_const.py @@ -50,6 +50,7 @@ RCCG_PREFIX = 'rccg-' HYPERCG_PREFIX = 'hycg-' VG_PREFIX = 'vg-' +VG_SNAPSHOT_PREFIX = 'vg_snap-' # remote mirror copy status REP_CONSIS_SYNC = 'consistent_synchronized' diff --git a/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py b/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py index 94f8fb11262..170f300e925 100644 --- a/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py +++ b/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py @@ -586,6 +586,64 @@ class StorwizeSSH(object): with excutils.save_and_reraise_exception(): LOG.exception('Failed to delete volumegroup.') + def lsvolumegroupsnapshot(self, params): + """Return volumegroup-snapshot attributes. + + Return None if it doesn't exists + """ + + ssh_cmd = ['svcinfo', 'lsvolumegroupsnapshot'] + if "id" in params: + ssh_cmd.append(params["id"]) + elif "name" and "volumegroup" in params: + ssh_cmd.extend(['-snapshot', params["name"], '-volumegroup', + params["volumegroup"]]) + # Add delimiter to parse the output + ssh_cmd.extend(['-delim', ':']) + out, err = self._ssh(ssh_cmd, check_exit_code=False) + if not err: + if not out: + return None + # Parse the lsvolumegroupsnapshot output + output = out.split('\n') + attributes = output[0].split(":") + attribute_values = output[1].split(":") + attrs = {key: val for key, val in zip(attributes, + attribute_values)} + return attrs + # CMMVC5804E implies volumegroup-snapshot or volumegroup specified + # does not exist in the SVC storage. + if 'CMMVC5804E' in err: + return None + msg = (_('CLI Exception output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': out, + 'err': err}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def addsnapshot(self, params): + ssh_cmd = ['svctask', 'addsnapshot', '-ignorelegacy'] + if "volumegroup" in params: + ssh_cmd.extend(['-volumegroup', params["volumegroup"]]) + if "name" in params: + ssh_cmd.extend(['-name', params["name"]]) + try: + return self.run_ssh_check_created(ssh_cmd) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Failed to create volumegroup snapshot.') + + def rmsnapshot(self, params): + ssh_cmd = ['svctask', 'rmsnapshot'] + if "id" in params: + ssh_cmd.extend(['-snapshotid', params["id"]]) + elif "name" and "volumegroup" in params: + ssh_cmd.extend(['-snapshot', params["name"], '-volumegroup', + params["volumegroup"]]) + self.run_ssh_assert_no_output(ssh_cmd) + def mkvdisk(self, name, size, units, pool, opts, params): ssh_cmd = ['svctask', 'mkvdisk', '-name', '"%s"' % name, '-mdiskgrp', '"%s"' % pool, '-iogrp', six.text_type(opts['iogrp']), @@ -2844,6 +2902,21 @@ class StorwizeHelpers(object): LOG.error(msg) raise exception.VolumeDriverException(message=msg) + def create_volumegroup_snapshot(self, params): + self.ssh.addsnapshot(params) + + def is_volumegroup_snapshot_exists(self, params): + """Check if volumegroup snapshot exists.""" + attrs = self.ssh.lsvolumegroupsnapshot(params) + return attrs is not None + + def delete_volumegroup_snapshot(self, params): + """Delete volumegroup snapshot""" + if not self.is_volumegroup_snapshot_exists(params): + LOG.info('Tried to delete non-existent volumegroup snapshot.') + return + self.ssh.rmsnapshot(params) + def get_partnership_info(self, system_name): partnership = self.ssh.lspartnership(system_name) return partnership[0] if len(partnership) > 0 else None @@ -3795,6 +3868,16 @@ class StorwizeSVCCommonDriver(san.SanDriver, volume.metadata['Volume Group Name'] = volumegroup_name volume.save() + def _update_volumegroup_snapshot_properties(self, ctxt, snapshot, + group_snapshot=None): + volumegroup_snapshot_name = ( + self._get_volumegroup_snapshot_name(group_snapshot) + if group_snapshot else "") + if not snapshot.metadata: + snapshot.metadata = dict() + snapshot.metadata['snapshot_name'] = volumegroup_snapshot_name + snapshot.save() + def create_volume(self, volume): LOG.debug('enter: create_volume: volume %s', volume['name']) # Create a replication or hyperswap volume with group_id is not @@ -6080,6 +6163,13 @@ class StorwizeSVCCommonDriver(san.SanDriver, vg = storwize_const.VG_PREFIX return vg + group_id[0:4] + '-' + group_id[-5:] + @staticmethod + def _get_volumegroup_snapshot_name(group_snapshot, grp_snapshot_id=None): + group_snapshot_id = ( + group_snapshot.id if group_snapshot else grp_snapshot_id) + vg_snapshot = storwize_const.VG_SNAPSHOT_PREFIX + return vg_snapshot + group_snapshot_id + # Add CG capability to generic volume groups def create_group(self, context, group): """Creates a group. @@ -6443,27 +6533,57 @@ class StorwizeSVCCommonDriver(san.SanDriver, :param snapshots: a list of Snapshot objects in the group_snapshot. :returns: model_update, snapshots_model_update """ - if (not volume_utils.is_group_a_cg_snapshot_type(group_snapshot) and - not volume_utils.is_group_a_type + + if (volume_utils.is_group_a_cg_snapshot_type(group_snapshot) or + volume_utils.is_group_a_type (group_snapshot, "consistent_group_replication_enabled") - and not volume_utils.is_group_a_type( + or volume_utils.is_group_a_type( group_snapshot, "hyperswap_group_enabled")): + # Use group_snapshot id as cg name + cg_name = 'cg_snap-' + group_snapshot.id + # Create new cg as cg_snapshot + self._helpers.create_fc_consistgrp(cg_name) + + timeout = self.configuration.storwize_svc_flashcopy_timeout + + model_update, snapshots_model = ( + self._helpers.run_consistgrp_snapshots(cg_name, + snapshots, + self._state, + self.configuration, + timeout)) + elif volume_utils.is_group_a_type( + group_snapshot, "volume_group_enabled"): + try: + self._helpers.check_codelevel_for_volumegroup( + self._state['code_level']) + params = dict() + # Use group_snapshot id as volumegroup name + volumegroup_snapshot_name = ( + self._get_volumegroup_snapshot_name(group_snapshot)) + params["name"] = volumegroup_snapshot_name + volumegroup_name = self._get_volumegroup_name( + None, grp_id=group_snapshot.group_id) + params["volumegroup"] = volumegroup_name + model_update = {'status': fields.GroupSnapshotStatus.AVAILABLE} + snapshots_model = [] + self._helpers.create_volumegroup_snapshot(params) + except exception.VolumeBackendAPIException as err: + model_update['status'] = fields.GroupSnapshotStatus.ERROR + LOG.error("Failed to create VolumeGroup Snapshot. " + "Exception: %s.", err) + for snapshot in snapshots: + self._update_volumegroup_snapshot_properties( + context, snapshot, group_snapshot) + snapshots_model.append( + {'id': snapshot['id'], + 'status': model_update['status'], + 'replication_status': fields.ReplicationStatus.NOT_CAPABLE + }) + else: # we'll rely on the generic group implementation if it is not a - # consistency group request. + # consistency group/volumegroup request. raise NotImplementedError() - # Use group_snapshot id as cg name - cg_name = 'cg_snap-' + group_snapshot.id - # Create new cg as cg_snapshot - self._helpers.create_fc_consistgrp(cg_name) - - timeout = self.configuration.storwize_svc_flashcopy_timeout - - model_update, snapshots_model = ( - self._helpers.run_consistgrp_snapshots(cg_name, - snapshots, - self._state, - self.configuration, - timeout)) return model_update, snapshots_model @@ -6476,20 +6596,53 @@ class StorwizeSVCCommonDriver(san.SanDriver, :returns: model_update, snapshots_model_update """ - if (not volume_utils.is_group_a_cg_snapshot_type(group_snapshot) and - not volume_utils.is_group_a_type( + if (volume_utils.is_group_a_cg_snapshot_type(group_snapshot) or + volume_utils.is_group_a_type( group_snapshot, "hyperswap_group_enabled")): + + cgsnapshot_id = group_snapshot.id + cg_name = 'cg_snap-' + cgsnapshot_id + + model_update, snapshots_model = ( + self._helpers.delete_consistgrp_snapshots(cg_name, + snapshots)) + elif volume_utils.is_group_a_type( + group_snapshot, "volume_group_enabled"): + try: + self._helpers.check_codelevel_for_volumegroup( + self._state['code_level']) + params = dict() + volumegroup_snapshot_name = ( + self._get_volumegroup_snapshot_name(group_snapshot)) + params["name"] = volumegroup_snapshot_name + volumegroup_name = self._get_volumegroup_name( + None, grp_id=group_snapshot.group_id) + params["volumegroup"] = volumegroup_name + model_update = {'status': fields.GroupSnapshotStatus.DELETED} + snapshots_model = [] + self._helpers.delete_volumegroup_snapshot(params) + for snapshot in snapshots: + self._update_volumegroup_snapshot_properties( + context, snapshot) + snapshots_model.append( + {'id': snapshot['id'], + 'status': fields.GroupSnapshotStatus.DELETED}) + except exception.VolumeBackendAPIException as err: + model_update['status'] = ( + fields.GroupSnapshotStatus.ERROR_DELETING) + for snapshot in snapshots: + snapshots_model.append( + {'id': snapshot['id'], + 'status': fields.GroupSnapshotStatus.ERROR_DELETING}) + LOG.error("Failed to delete the volume_group_snapshot %(snap) " + "with Exception: %(exception)s.", + {'snap': group_snapshot.group_id, 'exception': err}) + + else: # we'll rely on the generic group implementation if it is not a - # consistency group request. + # consistency group/volumegroup request. raise NotImplementedError() - cgsnapshot_id = group_snapshot.id - cg_name = 'cg_snap-' + cgsnapshot_id - - model_update, snapshots_model = ( - self._helpers.delete_consistgrp_snapshots(cg_name, - snapshots)) - return model_update, snapshots_model @volume_utils.trace diff --git a/releasenotes/notes/ibm-svf-volumegroup-snapshot-support-0a16d8a065501d66.yaml b/releasenotes/notes/ibm-svf-volumegroup-snapshot-support-0a16d8a065501d66.yaml new file mode 100644 index 00000000000..8d9f3dd2308 --- /dev/null +++ b/releasenotes/notes/ibm-svf-volumegroup-snapshot-support-0a16d8a065501d66.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + IBM Spectrum Virtualize Family driver: Added support for creation + and deletion of volumegroup snapshots.