Allow for eradicating Pure volumes on Cinder delete

This change adds in a config option which, when enabled, will cause
the Pure Storage volume drivers to eradicate volumes, snapshots, and
protection groups on delete for their Cinder counterpart.

With this config enabled an admin will NOT be able to recover data once
objects are deleted. By default the new config option is disabled and
volumes, snapshots, and protection group snapshots will be retained
pending eradication where they can be recovered for a period of time.

DocImpact
Closes-Bug: #1537187

Change-Id: Ib3656bb64e5104f6bd05d28535cf91a46a038b55
This commit is contained in:
Patrick East 2016-02-02 11:08:39 -08:00
parent 4cfed22bf8
commit b85caca74a
3 changed files with 70 additions and 2 deletions

View File

@ -338,8 +338,8 @@ class PureDriverTestCase(test.TestCase):
self.mock_config.pure_api_token = API_TOKEN self.mock_config.pure_api_token = API_TOKEN
self.mock_config.volume_backend_name = VOLUME_BACKEND_NAME self.mock_config.volume_backend_name = VOLUME_BACKEND_NAME
self.mock_config.safe_get.return_value = None self.mock_config.safe_get.return_value = None
self.mock_config.pure_eradicate_on_delete = False
self.array = mock.Mock() self.array = mock.Mock()
self.array
self.array.get.return_value = GET_ARRAY_PRIMARY self.array.get.return_value = GET_ARRAY_PRIMARY
self.array.array_name = GET_ARRAY_PRIMARY["array_name"] self.array.array_name = GET_ARRAY_PRIMARY["array_name"]
self.array.array_id = GET_ARRAY_PRIMARY["id"] self.array.array_id = GET_ARRAY_PRIMARY["id"]
@ -650,6 +650,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
) )
self.driver.delete_volume(VOLUME) self.driver.delete_volume(VOLUME)
self.assertFalse(self.array.destroy_volume.called) self.assertFalse(self.array.destroy_volume.called)
self.assertFalse(self.array.eradicate_volume.called)
# Testing case where array.destroy_volume returns an exception # Testing case where array.destroy_volume returns an exception
# because volume has already been deleted # because volume has already been deleted
@ -662,6 +663,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
) )
self.driver.delete_volume(VOLUME) self.driver.delete_volume(VOLUME)
self.assertTrue(self.array.destroy_volume.called) self.assertTrue(self.array.destroy_volume.called)
self.assertFalse(self.array.eradicate_volume.called)
def test_delete_volume(self): def test_delete_volume(self):
vol_name = VOLUME["name"] + "-cinder" vol_name = VOLUME["name"] + "-cinder"
@ -669,6 +671,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.driver.delete_volume(VOLUME) self.driver.delete_volume(VOLUME)
expected = [mock.call.destroy_volume(vol_name)] expected = [mock.call.destroy_volume(vol_name)]
self.array.assert_has_calls(expected) self.array.assert_has_calls(expected)
self.assertFalse(self.array.eradicate_volume.called)
self.array.destroy_volume.side_effect = ( self.array.destroy_volume.side_effect = (
self.purestorage_module.PureHTTPError(code=400, text="does not " self.purestorage_module.PureHTTPError(code=400, text="does not "
"exist")) "exist"))
@ -677,6 +680,15 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.assert_error_propagates([self.array.destroy_volume], self.assert_error_propagates([self.array.destroy_volume],
self.driver.delete_volume, VOLUME) self.driver.delete_volume, VOLUME)
def test_delete_volume_eradicate_now(self):
vol_name = VOLUME["name"] + "-cinder"
self.array.list_volume_private_connections.return_value = {}
self.mock_config.pure_eradicate_on_delete = True
self.driver.delete_volume(VOLUME)
expected = [mock.call.destroy_volume(vol_name),
mock.call.eradicate_volume(vol_name)]
self.array.assert_has_calls(expected)
def test_delete_connected_volume(self): def test_delete_connected_volume(self):
vol_name = VOLUME["name"] + "-cinder" vol_name = VOLUME["name"] + "-cinder"
host_name_a = "ha" host_name_a = "ha"
@ -715,6 +727,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.driver.delete_snapshot(SNAPSHOT) self.driver.delete_snapshot(SNAPSHOT)
expected = [mock.call.destroy_volume(snap_name)] expected = [mock.call.destroy_volume(snap_name)]
self.array.assert_has_calls(expected) self.array.assert_has_calls(expected)
self.assertFalse(self.array.eradicate_volume.called)
self.array.destroy_volume.side_effect = ( self.array.destroy_volume.side_effect = (
self.purestorage_module.PureHTTPError(code=400, text="does not " self.purestorage_module.PureHTTPError(code=400, text="does not "
"exist")) "exist"))
@ -723,6 +736,14 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.assert_error_propagates([self.array.destroy_volume], self.assert_error_propagates([self.array.destroy_volume],
self.driver.delete_snapshot, SNAPSHOT) self.driver.delete_snapshot, SNAPSHOT)
def test_delete_snapshot_eradicate_now(self):
snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"]
self.mock_config.pure_eradicate_on_delete = True
self.driver.delete_snapshot(SNAPSHOT)
expected = [mock.call.destroy_volume(snap_name),
mock.call.eradicate_volume(snap_name)]
self.array.assert_has_calls(expected)
@mock.patch(BASE_DRIVER_OBJ + "._get_host", autospec=True) @mock.patch(BASE_DRIVER_OBJ + "._get_host", autospec=True)
def test_terminate_connection(self, mock_host): def test_terminate_connection(self, mock_host):
vol_name = VOLUME["name"] + "-cinder" vol_name = VOLUME["name"] + "-cinder"
@ -967,6 +988,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
expected_name = self.driver._get_pgroup_name_from_id(mock_cgroup.id) expected_name = self.driver._get_pgroup_name_from_id(mock_cgroup.id)
self.array.destroy_pgroup.assert_called_with(expected_name) self.array.destroy_pgroup.assert_called_with(expected_name)
self.assertFalse(self.array.eradicate_pgroup.called)
expected_volume_updates = [{ expected_volume_updates = [{
'id': mock_volume.id, 'id': mock_volume.id,
@ -985,6 +1007,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
mock_cgroup, mock_cgroup,
[mock_volume]) [mock_volume])
self.array.destroy_pgroup.assert_called_with(expected_name) self.array.destroy_pgroup.assert_called_with(expected_name)
self.assertFalse(self.array.eradicate_pgroup.called)
mock_delete_volume.assert_called_with(self.driver, mock_volume) mock_delete_volume.assert_called_with(self.driver, mock_volume)
self.array.destroy_pgroup.side_effect = \ self.array.destroy_pgroup.side_effect = \
@ -996,6 +1019,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
mock_cgroup, mock_cgroup,
[mock_volume]) [mock_volume])
self.array.destroy_pgroup.assert_called_with(expected_name) self.array.destroy_pgroup.assert_called_with(expected_name)
self.assertFalse(self.array.eradicate_pgroup.called)
mock_delete_volume.assert_called_with(self.driver, mock_volume) mock_delete_volume.assert_called_with(self.driver, mock_volume)
self.array.destroy_pgroup.side_effect = \ self.array.destroy_pgroup.side_effect = \
@ -1146,6 +1170,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
[mock_snap]) [mock_snap])
self.array.destroy_pgroup.assert_called_with(snap_name) self.array.destroy_pgroup.assert_called_with(snap_name)
self.assertFalse(self.array.eradicate_pgroup.called)
self.assertEqual({'status': mock_cgsnap.status}, model_update) self.assertEqual({'status': mock_cgsnap.status}, model_update)
expected_snapshot_update = [{ expected_snapshot_update = [{
@ -1161,6 +1186,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
) )
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap, [mock_snap]) self.driver.delete_cgsnapshot(mock_context, mock_cgsnap, [mock_snap])
self.array.destroy_pgroup.assert_called_with(snap_name) self.array.destroy_pgroup.assert_called_with(snap_name)
self.assertFalse(self.array.eradicate_pgroup.called)
self.array.destroy_pgroup.side_effect = \ self.array.destroy_pgroup.side_effect = \
self.purestorage_module.PureHTTPError( self.purestorage_module.PureHTTPError(
@ -1169,6 +1195,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
) )
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap, [mock_snap]) self.driver.delete_cgsnapshot(mock_context, mock_cgsnap, [mock_snap])
self.array.destroy_pgroup.assert_called_with(snap_name) self.array.destroy_pgroup.assert_called_with(snap_name)
self.assertFalse(self.array.eradicate_pgroup.called)
self.array.destroy_pgroup.side_effect = \ self.array.destroy_pgroup.side_effect = \
self.purestorage_module.PureHTTPError( self.purestorage_module.PureHTTPError(
@ -1202,6 +1229,20 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
[mock_snap] [mock_snap]
) )
@mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name",
spec=pure.PureBaseVolumeDriver._get_pgroup_snap_name)
def test_delete_cgsnapshot_eradicate_now(self, mock_get_snap_name):
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
mock_get_snap_name.return_value = snap_name
self.mock_config.pure_eradicate_on_delete = True
model_update, snapshots = self.driver.delete_cgsnapshot(mock.Mock(),
mock.Mock(),
[mock.Mock()])
self.array.destroy_pgroup.assert_called_once_with(snap_name)
self.array.eradicate_pgroup.assert_called_once_with(snap_name)
def test_manage_existing(self): def test_manage_existing(self):
ref_name = 'vol1' ref_name = 'vol1'
volume_ref = {'name': ref_name} volume_ref = {'name': ref_name}

View File

@ -66,6 +66,15 @@ PURE_OPTS = [
cfg.IntOpt("pure_replica_retention_long_term_default", default=7, cfg.IntOpt("pure_replica_retention_long_term_default", default=7,
help="Retain snapshots per day on target for this time " help="Retain snapshots per day on target for this time "
"(in days.)"), "(in days.)"),
cfg.BoolOpt("pure_eradicate_on_delete",
default=False,
help="When enabled, all Pure volumes, snapshots, and "
"protection groups will be eradicated at the time of "
"deletion in Cinder. Data will NOT be recoverable after "
"a delete with this set to True! When disabled, volumes "
"and snapshots will go into pending eradication state "
"and can be recovered."
)
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -348,6 +357,8 @@ class PureBaseVolumeDriver(san.SanDriver):
host_name = host_info["host"] host_name = host_info["host"]
self._disconnect_host(current_array, host_name, vol_name) self._disconnect_host(current_array, host_name, vol_name)
current_array.destroy_volume(vol_name) current_array.destroy_volume(vol_name)
if self.configuration.pure_eradicate_on_delete:
current_array.eradicate_volume(vol_name)
except purestorage.PureHTTPError as err: except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt: with excutils.save_and_reraise_exception() as ctxt:
if (err.code == 400 and if (err.code == 400 and
@ -377,6 +388,8 @@ class PureBaseVolumeDriver(san.SanDriver):
snap_name = self._get_snap_name(snapshot) snap_name = self._get_snap_name(snapshot)
try: try:
current_array.destroy_volume(snap_name) current_array.destroy_volume(snap_name)
if self.configuration.pure_eradicate_on_delete:
current_array.eradicate_volume(snap_name)
except purestorage.PureHTTPError as err: except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt: with excutils.save_and_reraise_exception() as ctxt:
if err.code == 400 and ( if err.code == 400 and (
@ -654,7 +667,10 @@ class PureBaseVolumeDriver(san.SanDriver):
"""Deletes a consistency group.""" """Deletes a consistency group."""
try: try:
self._array.destroy_pgroup(self._get_pgroup_name_from_id(group.id)) pgroup_name = self._get_pgroup_name_from_id(group.id)
self._array.destroy_pgroup(pgroup_name)
if self.configuration.pure_eradicate_on_delete:
self._array.eradicate_pgroup(pgroup_name)
except purestorage.PureHTTPError as err: except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt: with excutils.save_and_reraise_exception() as ctxt:
if (err.code == 400 and if (err.code == 400 and
@ -724,6 +740,8 @@ class PureBaseVolumeDriver(san.SanDriver):
# FlashArray.destroy_pgroup is also used for deleting # FlashArray.destroy_pgroup is also used for deleting
# pgroup snapshots. The underlying REST API is identical. # pgroup snapshots. The underlying REST API is identical.
self._array.destroy_pgroup(pgsnap_name) self._array.destroy_pgroup(pgsnap_name)
if self.configuration.pure_eradicate_on_delete:
self._array.eradicate_pgroup(pgsnap_name)
except purestorage.PureHTTPError as err: except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt: with excutils.save_and_reraise_exception() as ctxt:
if (err.code == 400 and if (err.code == 400 and

View File

@ -0,0 +1,9 @@
---
features:
- New config option for Pure Storage volume drivers pure_eradicate_on_delete.
When enabled will permanantly eradicate data instead of placing into
pending eradication state.
fixes:
- Allow for eradicating Pure Storage volumes, snapshots, and pgroups when
deleting their Cinder counterpart.