From 27c2689a22d61d868d10d9b77638acd9ff745903 Mon Sep 17 00:00:00 2001 From: Patrick East Date: Tue, 11 Aug 2015 14:08:29 -0700 Subject: [PATCH] Implement Clone CG in Pure Volume Drivers This adds support for the consisgroup-create-from-src method specifying a source consistency group. The driver will create a new Purity Protection Group, take a temporary snapshot of the source Protection Group, create new volumes from the consistent snapshot, and then add these new volumes into the newly created protection group. Implements: blueprint pure-clone-cg Change-Id: I3446c4a86027f1f50ada673ddb1f3b13b5e8f6cc --- cinder/tests/unit/test_pure.py | 63 ++++++++++++++++++++----- cinder/volume/drivers/pure.py | 85 ++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 31 deletions(-) diff --git a/cinder/tests/unit/test_pure.py b/cinder/tests/unit/test_pure.py index ded9990dd..166e1e801 100644 --- a/cinder/tests/unit/test_pure.py +++ b/cinder/tests/unit/test_pure.py @@ -286,8 +286,7 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): @mock.patch(BASE_DRIVER_OBJ + "._add_volume_to_consistency_group", autospec=True) @mock.patch(BASE_DRIVER_OBJ + "._extend_if_needed", autospec=True) - @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_vol_snap_name", - spec=pure.PureBaseVolumeDriver._get_pgroup_vol_snap_name) + @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name_from_snapshot") def test_create_volume_from_cgsnapshot(self, mock_get_snap_name, mock_extend_if_needed, mock_add_to_cgroup): @@ -634,7 +633,8 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): "vol": volume_name, } - actual_name = self.driver._get_pgroup_vol_snap_name(mock_snap) + actual_name = self.driver._get_pgroup_snap_name_from_snapshot( + mock_snap) self.assertEqual(expected_name, actual_name) @@ -654,8 +654,8 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): @mock.patch(BASE_DRIVER_OBJ + ".create_volume_from_snapshot") @mock.patch(BASE_DRIVER_OBJ + ".create_consistencygroup") - def test_create_consistencygroup_from_src(self, mock_create_cg, - mock_create_vol): + def test_create_consistencygroup_from_cgsnapshot(self, mock_create_cg, + mock_create_vol): mock_context = mock.Mock() mock_group = mock.Mock() mock_cgsnapshot = mock.Mock() @@ -688,13 +688,52 @@ class PureBaseVolumeDriverTestCase(PureDriverTestCase): source_vols=None ) - def test_create_consistencygroup_from_src_no_snap(self): - # Expect an error when no cgsnapshot or snapshots are provided - self.assertRaises(exception.InvalidInput, - self.driver.create_consistencygroup_from_src, - mock.Mock(), # context - mock.Mock(), # group - [mock.Mock()]) # volumes + @mock.patch(BASE_DRIVER_OBJ + ".create_consistencygroup") + def test_create_consistencygroup_from_cg(self, mock_create_cg): + num_volumes = 5 + mock_context = mock.MagicMock() + mock_group = mock.MagicMock() + mock_source_cg = mock.MagicMock() + mock_volumes = [mock.MagicMock() for i in range(num_volumes)] + mock_source_vols = [mock.MagicMock() for i in range(num_volumes)] + self.driver.create_consistencygroup_from_src( + mock_context, + mock_group, + mock_volumes, + source_cg=mock_source_cg, + source_vols=mock_source_vols + ) + mock_create_cg.assert_called_with(mock_context, mock_group) + self.assertTrue(self.array.create_pgroup_snapshot.called) + self.assertEqual(num_volumes, self.array.copy_volume.call_count) + self.assertEqual(num_volumes, self.array.set_pgroup.call_count) + self.assertTrue(self.array.destroy_pgroup.called) + + @mock.patch(BASE_DRIVER_OBJ + ".create_consistencygroup") + def test_create_consistencygroup_from_cg_with_error(self, mock_create_cg): + num_volumes = 5 + mock_context = mock.MagicMock() + mock_group = mock.MagicMock() + mock_source_cg = mock.MagicMock() + mock_volumes = [mock.MagicMock() for i in range(num_volumes)] + mock_source_vols = [mock.MagicMock() for i in range(num_volumes)] + + self.array.copy_volume.side_effect = FakePureStorageHTTPError() + + self.assertRaises( + FakePureStorageHTTPError, + self.driver.create_consistencygroup_from_src, + mock_context, + mock_group, + mock_volumes, + source_cg=mock_source_cg, + source_vols=mock_source_vols + ) + mock_create_cg.assert_called_with(mock_context, mock_group) + self.assertTrue(self.array.create_pgroup_snapshot.called) + # Make sure that the temp snapshot is cleaned up even when copying + # the volume fails! + self.assertTrue(self.array.destroy_pgroup.called) @mock.patch(BASE_DRIVER_OBJ + ".delete_volume", autospec=True) def test_delete_consistencygroup(self, mock_delete_volume): diff --git a/cinder/volume/drivers/pure.py b/cinder/volume/drivers/pure.py index 10053ac62..7be2c8f66 100644 --- a/cinder/volume/drivers/pure.py +++ b/cinder/volume/drivers/pure.py @@ -130,7 +130,7 @@ class PureBaseVolumeDriver(san.SanDriver): """Creates a volume from a snapshot.""" vol_name = self._get_vol_name(volume) if snapshot['cgsnapshot_id']: - snap_name = self._get_pgroup_vol_snap_name(snapshot) + snap_name = self._get_pgroup_snap_name_from_snapshot(snapshot) else: snap_name = self._get_snap_name(snapshot) @@ -336,19 +336,57 @@ class PureBaseVolumeDriver(san.SanDriver): model_update = {'status': 'available'} return model_update + def _create_cg_from_cgsnap(self, volumes, snapshots): + """Creates a new consistency group from a cgsnapshot. + + The new volumes will be consistent with the snapshot. + """ + for volume, snapshot in zip(volumes, snapshots): + self.create_volume_from_snapshot(volume, snapshot) + + def _create_cg_from_cg(self, group, source_group, volumes, source_vols): + """Creates a new consistency group from an existing cg. + + The new volumes will be in a consistent state, but this requires + taking a new temporary group snapshot and cloning from that. + """ + pgroup_name = self._get_pgroup_name_from_id(source_group.id) + tmp_suffix = '%s-tmp' % uuid.uuid4() + tmp_pgsnap_name = '%(pgroup_name)s.%(pgsnap_suffix)s' % { + 'pgroup_name': pgroup_name, + 'pgsnap_suffix': tmp_suffix, + } + LOG.debug('Creating temporary Protection Group snapshot %(snap_name)s ' + 'while cloning Consistency Group %(source_group)s.', + {'snap_name': tmp_pgsnap_name, + 'source_group': source_group.id}) + self._array.create_pgroup_snapshot(pgroup_name, suffix=tmp_suffix) + try: + for source_vol, cloned_vol in zip(source_vols, volumes): + source_snap_name = self._get_pgroup_vol_snap_name( + pgroup_name, + tmp_suffix, + self._get_vol_name(source_vol) + ) + cloned_vol_name = self._get_vol_name(cloned_vol) + self._array.copy_volume(source_snap_name, cloned_vol_name) + self._add_volume_to_consistency_group( + group.id, + cloned_vol_name + ) + finally: + self._delete_pgsnapshot(tmp_pgsnap_name) + @log_debug_trace def create_consistencygroup_from_src(self, context, group, volumes, cgsnapshot=None, snapshots=None, source_cg=None, source_vols=None): - + self.create_consistencygroup(context, group) if cgsnapshot and snapshots: - self.create_consistencygroup(context, group) - for volume, snapshot in zip(volumes, snapshots): - self.create_volume_from_snapshot(volume, snapshot) - else: - msg = _("create_consistencygroup_from_src only supports a" - " cgsnapshot source, other sources cannot be used.") - raise exception.InvalidInput(reason=msg) + self._create_cg_from_cgsnap(volumes, + snapshots) + elif source_cg: + self._create_cg_from_cg(group, source_cg, volumes, source_vols) return None, None @@ -418,12 +456,7 @@ class PureBaseVolumeDriver(san.SanDriver): return model_update, snapshots - @log_debug_trace - def delete_cgsnapshot(self, context, cgsnapshot): - """Deletes a cgsnapshot.""" - - pgsnap_name = self._get_pgroup_snap_name(cgsnapshot) - + def _delete_pgsnapshot(self, pgsnap_name): try: # FlashArray.destroy_pgroup is also used for deleting # pgroup snapshots. The underlying REST API is identical. @@ -439,6 +472,13 @@ class PureBaseVolumeDriver(san.SanDriver): LOG.warning(_LW("Unable to delete Protection Group " "Snapshot: %s"), err.text) + @log_debug_trace + def delete_cgsnapshot(self, context, cgsnapshot): + """Deletes a cgsnapshot.""" + + pgsnap_name = self._get_pgroup_snap_name(cgsnapshot) + self._delete_pgsnapshot(pgsnap_name) + snapshots = objects.SnapshotList.get_all_for_cgsnapshot( context, cgsnapshot.id) @@ -564,12 +604,19 @@ class PureBaseVolumeDriver(san.SanDriver): return "%s.%s" % (cls._get_pgroup_name_from_id(cg_id), cls._get_pgroup_snap_suffix(cgsnapshot)) - @classmethod - def _get_pgroup_vol_snap_name(cls, snapshot): + @staticmethod + def _get_pgroup_vol_snap_name(pg_name, pgsnap_suffix, volume_name): + return "%(pgroup_name)s.%(pgsnap_suffix)s.%(volume_name)s" % { + 'pgroup_name': pg_name, + 'pgsnap_suffix': pgsnap_suffix, + 'volume_name': volume_name, + } + + def _get_pgroup_snap_name_from_snapshot(self, snapshot): """Return the name of the snapshot that Purity will use.""" cg_id = snapshot.cgsnapshot.consistencygroup_id - cg_name = cls._get_pgroup_name_from_id(cg_id) - cgsnapshot_id = cls._get_pgroup_snap_suffix(snapshot.cgsnapshot) + cg_name = self._get_pgroup_name_from_id(cg_id) + cgsnapshot_id = self._get_pgroup_snap_suffix(snapshot.cgsnapshot) volume_name = snapshot.volume_name return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name) -- 2.45.2