From 304ff4c23db878262a553d0d15771c0beb970b42 Mon Sep 17 00:00:00 2001 From: TommyLike Date: Fri, 21 Oct 2016 15:47:03 +0800 Subject: [PATCH] [2/4]Reset group snapshot status Currently the administrator could only reset the group snapshot status by db operation, this change intends to add new admin action to achieve this. The patch list: 1. group API(https://review.openstack.org/#/c/389091/). 2. group snapshot API(this). 3. cinder client(https://review.openstack.org/390169/). 4. documentation(https://review.openstack.org/#/c/395464/). APIImpact DocImpact Partial-Implements: blueprint reset-cg-and-cgs-status Change-Id: I9e3a26950c435038cf40bea4b27aea1bd5049e95 --- cinder/api/openstack/api_version_request.py | 3 +- .../openstack/rest_api_version_history.rst | 4 ++ cinder/api/v3/group_snapshots.py | 44 ++++++++++++ cinder/api/v3/router.py | 8 ++- cinder/exception.py | 4 ++ cinder/group/api.py | 15 ++++ cinder/objects/fields.py | 17 +++++ cinder/tests/functional/api/client.py | 4 ++ .../tests/functional/test_group_snapshots.py | 53 +++++++++++++- .../tests/unit/api/v3/test_group_snapshots.py | 71 ++++++++++++++++++- cinder/tests/unit/group/test_groups_api.py | 13 ++++ cinder/tests/unit/policy.json | 1 + etc/cinder/policy.json | 1 + ...roup-snapshot-status-sd21a31cde5fa035.yaml | 3 + 14 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-reset-group-snapshot-status-sd21a31cde5fa035.yaml diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index f49d13809d8..b806b6eb8e1 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -67,6 +67,7 @@ REST_API_VERSION_HISTORY = """ * 3.16 - Migrate volume now supports cluster * 3.17 - Getting manageable volumes and snapshots now accepts cluster. * 3.18 - Add backup project attribute. + * 3.19 - Add API reset status actions 'reset_status' to group snapshot. """ # The minimum and maximum versions of the API supported @@ -74,7 +75,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 enpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.18" +_MAX_API_VERSION = "3.19" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 488d7dead60..075190e18e9 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -210,3 +210,7 @@ user documentation. 3.18 ---- Added backup project attribute. + +3.19 +---- + Added reset status actions 'reset_status' to group snapshot. diff --git a/cinder/api/v3/group_snapshots.py b/cinder/api/v3/group_snapshots.py index 0da9dd3ecfc..0e43c68549a 100644 --- a/cinder/api/v3/group_snapshots.py +++ b/cinder/api/v3/group_snapshots.py @@ -26,6 +26,7 @@ from cinder.api.v3.views import group_snapshots as group_snapshot_views from cinder import exception from cinder import group as group_api from cinder.i18n import _, _LI +from cinder import rpc LOG = logging.getLogger(__name__) @@ -141,6 +142,49 @@ class GroupSnapshotsController(wsgi.Controller): return retval + @wsgi.Controller.api_version('3.19') + @wsgi.action("reset_status") + def reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + def _reset_status(self, req, id, body): + """Reset status on group snapshots""" + + context = req.environ['cinder.context'] + try: + status = body['reset_status']['status'].lower() + except (TypeError, KeyError): + raise exc.HTTPBadRequest(explanation=_("Must specify 'status'")) + + LOG.debug("Updating group '%(id)s' with " + "'%(update)s'", {'id': id, + 'update': status}) + try: + notifier = rpc.get_notifier('groupSnapshotStatusUpdate') + notifier.info(context, 'groupsnapshots.reset_status.start', + {'id': id, + 'update': status}) + gsnapshot = self.group_snapshot_api.get_group_snapshot(context, id) + + self.group_snapshot_api.reset_group_snapshot_status(context, + gsnapshot, + status) + notifier.info(context, 'groupsnapshots.reset_status.end', + {'id': id, + 'update': status}) + except exception.GroupSnapshotNotFound as error: + # Not found exception will be handled at the wsgi level + notifier.error(context, 'groupsnapshots.reset_status', + {'error_message': error.msg, + 'id': id}) + raise + except exception.InvalidGroupSnapshotStatus as error: + notifier.error(context, 'groupsnapshots.reset_status', + {'error_message': error.msg, + 'id': id}) + raise exc.HTTPBadRequest(explanation=error.msg) + return webob.Response(status_int=202) + def create_resource(): return wsgi.Resource(GroupSnapshotsController()) diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index f46660a5a4c..f9d1e14422e 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -100,12 +100,16 @@ class APIRouter(cinder.api.openstack.APIRouter): action="action", conditions={"method": ["POST"]}) - self.resources['group_snapshots'] = (group_snapshots.create_resource()) + self.resources['group_snapshots'] = group_snapshots.create_resource() mapper.resource("group_snapshot", "group_snapshots", controller=self.resources['group_snapshots'], collection={'detail': 'GET'}, member={'action': 'POST'}) - + mapper.connect("group_snapshots", + "/{project_id}/group_snapshots/{id}/action", + controller=self.resources["group_snapshots"], + action="action", + conditions={"method": ["POST"]}) self.resources['snapshots'] = snapshots.create_resource(ext_mgr) mapper.resource("snapshot", "snapshots", controller=self.resources['snapshots'], diff --git a/cinder/exception.py b/cinder/exception.py index d3c2986220b..d4fa4d1b1d2 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1079,6 +1079,10 @@ class InvalidGroupSnapshot(Invalid): message = _("Invalid GroupSnapshot: %(reason)s") +class InvalidGroupSnapshotStatus(Invalid): + message = _("Invalid GroupSnapshot Status: %(reason)s") + + # Hitachi Block Storage Driver class HBSDError(VolumeDriverException): message = _("HBSD error occurs.") diff --git a/cinder/group/api.py b/cinder/group/api.py index 1a86a32dc3e..487ac5fd099 100644 --- a/cinder/group/api.py +++ b/cinder/group/api.py @@ -844,3 +844,18 @@ class API(base.Base): group_snapshots = objects.GroupSnapshotList.get_all_by_project( context.elevated(), context.project_id, search_opts) return group_snapshots + + def reset_group_snapshot_status(self, context, gsnapshot, status): + """Reset status of group snapshot""" + + check_policy(context, 'reset_group_snapshot_status') + if status not in c_fields.GroupSnapshotStatus.ALL: + msg = _("Group snapshot status: %(status)s is invalid, " + "valid statuses are: " + "%(valid)s.") % {'status': status, + 'valid': c_fields.GroupSnapshotStatus.ALL} + raise exception.InvalidGroupSnapshotStatus(reason=msg) + field = {'updated_at': timeutils.utcnow(), + 'status': status} + gsnapshot.update(field) + gsnapshot.save() diff --git a/cinder/objects/fields.py b/cinder/objects/fields.py index d22b86c3474..8000388f59e 100644 --- a/cinder/objects/fields.py +++ b/cinder/objects/fields.py @@ -80,6 +80,23 @@ class GroupStatusField(BaseEnumField): AUTO_TYPE = GroupStatus() +class GroupSnapshotStatus(BaseCinderEnum): + ERROR = 'error' + AVAILABLE = 'available' + CREATING = 'creating' + DELETING = 'deleting' + DELETED = 'deleted' + UPDATING = 'updating' + ERROR_DELETING = 'error_deleting' + + ALL = (ERROR, AVAILABLE, CREATING, DELETING, DELETED, + UPDATING, ERROR_DELETING) + + +class GroupSnapshotStatusField(BaseEnumField): + AUTO_TYPE = GroupSnapshotStatus() + + class ReplicationStatus(BaseCinderEnum): ERROR = 'error' ENABLED = 'enabled' diff --git a/cinder/tests/functional/api/client.py b/cinder/tests/functional/api/client.py index 7baeae9c7e0..8643582cd1b 100644 --- a/cinder/tests/functional/api/client.py +++ b/cinder/tests/functional/api/client.py @@ -270,3 +270,7 @@ class TestOpenStackClient(object): def delete_group_snapshot(self, group_snapshot_id): return self.api_delete('/group_snapshots/%s' % group_snapshot_id) + + def reset_group_snapshot(self, group_snapshot_id, params): + return self.api_post('/group_snapshots/%s/action' % group_snapshot_id, + params) diff --git a/cinder/tests/functional/test_group_snapshots.py b/cinder/tests/functional/test_group_snapshots.py index c9487dfb6df..088737e6a5f 100644 --- a/cinder/tests/functional/test_group_snapshots.py +++ b/cinder/tests/functional/test_group_snapshots.py @@ -20,7 +20,7 @@ class GroupSnapshotsTest(functional_helpers._FunctionalTestBase): _vol_type_name = 'functional_test_type' _grp_type_name = 'functional_grp_test_type' osapi_version_major = '3' - osapi_version_minor = '14' + osapi_version_minor = '19' def setUp(self): super(GroupSnapshotsTest, self).setUp() @@ -295,3 +295,54 @@ class GroupSnapshotsTest(functional_helpers._FunctionalTestBase): self.assertFalse(found_group_from_group) self.assertFalse(found_volume) self.assertFalse(found_group) + + def test_reset_group_snapshot(self): + # Create group + group1 = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) + self.assertTrue(group1['id']) + group_id = group1['id'] + self._poll_group_while(group_id, ['creating']) + + # Create volume + created_volume = self.api.post_volume( + {'volume': {'size': 1, + 'group_id': group_id, + 'volume_type': self.volume_type['id']}}) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + self._poll_volume_while(created_volume_id, ['creating']) + + # Create group snapshot + group_snapshot1 = self.api.post_group_snapshot( + {'group_snapshot': {'group_id': group_id}}) + self.assertTrue(group_snapshot1['id']) + group_snapshot_id = group_snapshot1['id'] + + self._poll_group_snapshot_while(group_snapshot_id, 'creating') + + group_snapshot1 = self.api.get_group_snapshot(group_snapshot_id) + self.assertEqual("available", group_snapshot1['status']) + + # reset group snapshot status + self.api.reset_group_snapshot(group_snapshot_id, + {"reset_status": {"status": "error"}}) + + group_snapshot1 = self.api.get_group_snapshot(group_snapshot_id) + self.assertEqual("error", group_snapshot1['status']) + + # Delete group, volume and group snapshot + self.api.delete_group_snapshot(group_snapshot_id) + found_group_snapshot = self._poll_group_snapshot_while( + group_snapshot_id, ['deleting']) + self.api.delete_group(group_id, + {'delete': {'delete-volumes': True}}) + + found_volume = self._poll_volume_while(created_volume_id, ['deleting']) + found_group = self._poll_group_while(group_id, ['deleting']) + + # Created resoueces should be gone + self.assertFalse(found_group_snapshot) + self.assertFalse(found_volume) + self.assertFalse(found_group) diff --git a/cinder/tests/unit/api/v3/test_group_snapshots.py b/cinder/tests/unit/api/v3/test_group_snapshots.py index 93f520b1b45..0e24dda0e12 100644 --- a/cinder/tests/unit/api/v3/test_group_snapshots.py +++ b/cinder/tests/unit/api/v3/test_group_snapshots.py @@ -17,6 +17,7 @@ Tests for group_snapshot code. """ +import ddt import mock import webob @@ -26,6 +27,7 @@ from cinder import db from cinder import exception from cinder.group import api as group_api from cinder import objects +from cinder.objects import fields from cinder import test from cinder.tests.unit.api import fakes from cinder.tests.unit import fake_constants as fake @@ -35,6 +37,7 @@ import cinder.volume GROUP_MICRO_VERSION = '3.14' +@ddt.ddt class GroupSnapshotsAPITestCase(test.TestCase): """Test Case for group_snapshots API.""" @@ -395,7 +398,7 @@ class GroupSnapshotsAPITestCase(test.TestCase): self.controller.delete, req, fake.WILL_NOT_BE_FOUND_ID) - def test_delete_group_snapshot_with_Invalid_group_snapshot(self): + def test_delete_group_snapshot_with_invalid_group_snapshot(self): group = utils.create_group( self.context, group_type_id=fake.GROUP_TYPE_ID, @@ -418,3 +421,69 @@ class GroupSnapshotsAPITestCase(test.TestCase): db.volume_destroy(context.get_admin_context(), volume_id) group.destroy() + + @ddt.data(('3.11', 'fake_snapshot_001', + fields.GroupSnapshotStatus.AVAILABLE, + exception.VersionNotFoundForAPIMethod), + ('3.18', 'fake_snapshot_001', + fields.GroupSnapshotStatus.AVAILABLE, + exception.VersionNotFoundForAPIMethod), + ('3.19', 'fake_snapshot_001', + fields.GroupSnapshotStatus.AVAILABLE, + exception.GroupSnapshotNotFound)) + @ddt.unpack + def test_reset_group_snapshot_status_illegal(self, version, + group_snapshot_id, + status, exceptions): + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s/action' % + (fake.PROJECT_ID, group_snapshot_id), + version=version) + body = {"reset_status": { + "status": status + }} + self.assertRaises(exceptions, + self.controller.reset_status, + req, group_snapshot_id, body) + + def test_reset_group_snapshot_status_invalid_status(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID]) + group_snapshot = utils.create_group_snapshot( + self.context, + group_id=group.id, + status=fields.GroupSnapshotStatus.CREATING) + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s/action' % + (fake.PROJECT_ID, group_snapshot.id), + version='3.19') + body = {"reset_status": { + "status": "invalid_test_status" + }} + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.reset_status, + req, group_snapshot.id, body) + + def test_reset_group_snapshot_status(self): + group = utils.create_group( + self.context, + group_type_id=fake.GROUP_TYPE_ID, + volume_type_ids=[fake.VOLUME_TYPE_ID]) + group_snapshot = utils.create_group_snapshot( + self.context, + group_id=group.id, + status=fields.GroupSnapshotStatus.CREATING) + req = fakes.HTTPRequest.blank('/v3/%s/group_snapshots/%s/action' % + (fake.PROJECT_ID, group_snapshot.id), + version='3.19') + body = {"reset_status": { + "status": fields.GroupSnapshotStatus.AVAILABLE + }} + response = self.controller.reset_status(req, group_snapshot.id, + body) + + g_snapshot = objects.GroupSnapshot.get_by_id(self.context, + group_snapshot.id) + self.assertEqual(202, response.status_int) + self.assertEqual(fields.GroupSnapshotStatus.AVAILABLE, + g_snapshot.status) diff --git a/cinder/tests/unit/group/test_groups_api.py b/cinder/tests/unit/group/test_groups_api.py index 5ffac7cb397..3cc82aa3b75 100644 --- a/cinder/tests/unit/group/test_groups_api.py +++ b/cinder/tests/unit/group/test_groups_api.py @@ -515,3 +515,16 @@ class GroupAPITestCase(test.TestCase): self.assertEqual(grp.obj_to_primitive(), ret_group.obj_to_primitive()) mock_create_from_snap.assert_called_once_with( self.ctxt, grp, fake.GROUP_SNAPSHOT_ID) + + @mock.patch('oslo_utils.timeutils.utcnow') + @mock.patch('cinder.objects.GroupSnapshot') + def test_reset_group_snapshot_status(self, mock_group_snapshot, + mock_time_util): + mock_time_util.return_value = "time_now" + self.group_api.reset_group_snapshot_status( + self.ctxt, mock_group_snapshot, fields.GroupSnapshotStatus.ERROR) + + update_field = {'updated_at': "time_now", + 'status': fields.GroupSnapshotStatus.ERROR} + mock_group_snapshot.update.assert_called_once_with(update_field) + mock_group_snapshot.save.assert_called_once_with() diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index 1b347a24400..f91245e9bdd 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -131,6 +131,7 @@ "group:update_group_snapshot": "", "group:get_group_snapshot": "", "group:get_all_group_snapshots": "", + "group:reset_group_snapshot_status":"", "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 139fe3c2312..da658d9f9ba 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -126,6 +126,7 @@ "group:update_group_snapshot": "rule:admin_or_owner", "group:get_group_snapshot": "rule:admin_or_owner", "group:get_all_group_snapshots": "rule:admin_or_owner", + "group:reset_group_snapshot_status":"rule:admin_api", "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", "message:delete": "rule:admin_or_owner", diff --git a/releasenotes/notes/add-reset-group-snapshot-status-sd21a31cde5fa035.yaml b/releasenotes/notes/add-reset-group-snapshot-status-sd21a31cde5fa035.yaml new file mode 100644 index 00000000000..5ace44c746a --- /dev/null +++ b/releasenotes/notes/add-reset-group-snapshot-status-sd21a31cde5fa035.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added reset status API to group snapshot.