diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index f49d13809..b806b6eb8 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 488d7dead..075190e18 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 0da9dd3ec..0e43c6854 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 f46660a5a..f9d1e1442 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 d3c298622..d4fa4d1b1 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 1a86a32dc..487ac5fd0 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 d22b86c34..8000388f5 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 7baeae9c7..8643582cd 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 c9487dfb6..088737e6a 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 93f520b1b..0e24dda0e 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 5ffac7cb3..3cc82aa3b 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 1b347a244..f91245e9b 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 139fe3c23..da658d9f9 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 000000000..5ace44c74 --- /dev/null +++ b/releasenotes/notes/add-reset-group-snapshot-status-sd21a31cde5fa035.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added reset status API to group snapshot.