diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index b806b6eb8e1..cbc698cc776 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -68,6 +68,8 @@ REST_API_VERSION_HISTORY = """ * 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. + * 3.20 - Add API reset status actions 'reset_status' to generic + volume group. """ # The minimum and maximum versions of the API supported @@ -75,7 +77,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.19" +_MAX_API_VERSION = "3.20" _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 075190e18e9..2ecc6c56a21 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -214,3 +214,7 @@ user documentation. 3.19 ---- Added reset status actions 'reset_status' to group snapshot. + +3.20 +---- + Added reset status actions 'reset_status' to generic volume group. diff --git a/cinder/api/v3/groups.py b/cinder/api/v3/groups.py index 3336222cd6f..17d5f5fd50f 100644 --- a/cinder/api/v3/groups.py +++ b/cinder/api/v3/groups.py @@ -26,6 +26,7 @@ from cinder.api.v3.views import groups as views_groups from cinder import exception from cinder import group as group_api from cinder.i18n import _, _LI +from cinder import rpc from cinder.volume import group_types LOG = logging.getLogger(__name__) @@ -65,6 +66,47 @@ class GroupsController(wsgi.Controller): return self._view_builder.detail(req, group) + @wsgi.Controller.api_version('3.20') + @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 generic group.""" + + 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('groupStatusUpdate') + notifier.info(context, 'groups.reset_status.start', + {'id': id, + 'update': status}) + group = self.group_api.get(context, id) + + self.group_api.reset_status(context, group, status) + notifier.info(context, 'groups.reset_status.end', + {'id': id, + 'update': status}) + except exception.GroupNotFound as error: + # Not found exception will be handled at the wsgi level + notifier.error(context, 'groups.reset_status', + {'error_message': error.msg, + 'id': id}) + raise + except exception.InvalidGroupStatus as error: + notifier.error(context, 'groups.reset_status', + {'error_message': error.msg, + 'id': id}) + raise exc.HTTPBadRequest(explanation=error.msg) + return webob.Response(status_int=202) + @wsgi.Controller.api_version(GROUP_API_VERSION) @wsgi.action("delete") def delete_group(self, req, id, body): diff --git a/cinder/exception.py b/cinder/exception.py index d4fa4d1b1d2..b24e888b525 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1061,6 +1061,10 @@ class InvalidGroup(Invalid): message = _("Invalid Group: %(reason)s") +class InvalidGroupStatus(Invalid): + message = _("Invalid Group Status: %(reason)s") + + # CgSnapshot class CgSnapshotNotFound(NotFound): message = _("CgSnapshot %(cgsnapshot_id)s could not be found.") diff --git a/cinder/group/api.py b/cinder/group/api.py index 231784ac4fc..00722329dce 100644 --- a/cinder/group/api.py +++ b/cinder/group/api.py @@ -772,6 +772,20 @@ class API(base.Base): sort_dirs=sort_dirs) return groups + def reset_status(self, context, group, status): + """Reset status of generic group""" + + check_policy(context, 'reset_status') + if status not in c_fields.GroupStatus.ALL: + msg = _("Group status: %(status)s is invalid, valid status " + "are: %(valid)s.") % {'status': status, + 'valid': c_fields.GroupStatus.ALL} + raise exception.InvalidGroupStatus(reason=msg) + field = {'updated_at': timeutils.utcnow(), + 'status': status} + group.update(field) + group.save() + def create_group_snapshot(self, context, group, name, description): options = {'group_id': group.id, 'user_id': context.user_id, diff --git a/cinder/tests/functional/api/client.py b/cinder/tests/functional/api/client.py index 8643582cd1b..c0fa162a54a 100644 --- a/cinder/tests/functional/api/client.py +++ b/cinder/tests/functional/api/client.py @@ -253,6 +253,9 @@ class TestOpenStackClient(object): def delete_group(self, group_id, params): return self.api_post('/groups/%s/action' % group_id, params) + def reset_group(self, group_id, params): + return self.api_post('/groups/%s/action' % group_id, params) + def put_group(self, group_id, group): return self.api_put('/groups/%s' % group_id, group)['group'] diff --git a/cinder/tests/functional/test_groups.py b/cinder/tests/functional/test_groups.py index 3ac69b305f8..b6492675601 100644 --- a/cinder/tests/functional/test_groups.py +++ b/cinder/tests/functional/test_groups.py @@ -20,12 +20,15 @@ class GroupsTest(functional_helpers._FunctionalTestBase): _vol_type_name = 'functional_test_type' _grp_type_name = 'functional_grp_test_type' osapi_version_major = '3' - osapi_version_minor = '13' + osapi_version_minor = '20' def setUp(self): super(GroupsTest, self).setUp() self.volume_type = self.api.create_type(self._vol_type_name) self.group_type = self.api.create_group_type(self._grp_type_name) + self.group1 = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) def _get_flags(self): f = super(GroupsTest, self)._get_flags() @@ -45,6 +48,17 @@ class GroupsTest(functional_helpers._FunctionalTestBase): grps = self.api.get_groups() self.assertIsNotNone(grps) + def test_reset_group_status(self): + """Reset group status""" + found_group = self._poll_group_while(self.group1['id'], + ['creating']) + self.assertEqual('available', found_group['status']) + self.api.reset_group(self.group1['id'], + {"reset_status": {"status": "error"}}) + + group = self.api.get_group(self.group1['id']) + self.assertEqual("error", group['status']) + def test_create_and_delete_group(self): """Creates and deletes a group.""" diff --git a/cinder/tests/unit/api/v3/test_groups.py b/cinder/tests/unit/api/v3/test_groups.py index e736acc746d..6c607582110 100644 --- a/cinder/tests/unit/api/v3/test_groups.py +++ b/cinder/tests/unit/api/v3/test_groups.py @@ -824,6 +824,47 @@ class GroupsAPITestCase(test.TestCase): self.controller.update, req, self.group1.id, body) + @ddt.data(('3.11', 'fake_group_001', + fields.GroupStatus.AVAILABLE, + exception.VersionNotFoundForAPIMethod), + ('3.19', 'fake_group_001', + fields.GroupStatus.AVAILABLE, + exception.VersionNotFoundForAPIMethod), + ('3.20', 'fake_group_001', + fields.GroupStatus.AVAILABLE, + exception.GroupNotFound), + ('3.20', None, + 'invalid_test_status', + webob.exc.HTTPBadRequest), + ) + @ddt.unpack + def test_reset_group_status_illegal(self, version, group_id, + status, exceptions): + g_id = group_id or self.group2.id + req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' % + (fake.PROJECT_ID, g_id), + version=version) + body = {"reset_status": { + "status": status + }} + self.assertRaises(exceptions, + self.controller.reset_status, + req, g_id, body) + + def test_reset_group_status(self): + req = fakes.HTTPRequest.blank('/v3/%s/groups/%s/action' % + (fake.PROJECT_ID, self.group2.id), + version='3.20') + body = {"reset_status": { + "status": fields.GroupStatus.AVAILABLE + }} + response = self.controller.reset_status(req, + self.group2.id, body) + + group = objects.Group.get_by_id(self.ctxt, self.group2.id) + self.assertEqual(202, response.status_int) + self.assertEqual(fields.GroupStatus.AVAILABLE, group.status) + @mock.patch( 'cinder.api.openstack.wsgi.Controller.validate_name_and_description') def test_create_group_from_src_snap(self, mock_validate): diff --git a/cinder/tests/unit/group/test_groups_api.py b/cinder/tests/unit/group/test_groups_api.py index dcd0d7d434c..a4c2ab9872f 100644 --- a/cinder/tests/unit/group/test_groups_api.py +++ b/cinder/tests/unit/group/test_groups_api.py @@ -173,6 +173,18 @@ class GroupAPITestCase(test.TestCase): mock_volume_types_get.assert_called_once_with(mock.ANY, volume_type_names) + @mock.patch('oslo_utils.timeutils.utcnow') + @mock.patch('cinder.objects.Group') + def test_reset_status(self, mock_group, mock_time_util): + mock_time_util.return_value = "time_now" + self.group_api.reset_status(self.ctxt, mock_group, + fields.GroupStatus.AVAILABLE) + + update_field = {'updated_at': "time_now", + 'status': fields.GroupStatus.AVAILABLE} + mock_group.update.assert_called_once_with(update_field) + mock_group.save.assert_called_once_with() + @mock.patch.object(GROUP_QUOTAS, "reserve") @mock.patch('cinder.objects.Group') @mock.patch('cinder.db.group_type_get_by_name') diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index f91245e9bdd..0179e1cc331 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -132,6 +132,7 @@ "group:get_group_snapshot": "", "group:get_all_group_snapshots": "", "group:reset_group_snapshot_status":"", + "group:reset_status":"", "scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api", diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index da658d9f9ba..26e90eab991 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -127,6 +127,7 @@ "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", + "group:reset_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-status-sd21a31cde5fa034.yaml b/releasenotes/notes/add-reset-group-status-sd21a31cde5fa034.yaml new file mode 100644 index 00000000000..08ea5945206 --- /dev/null +++ b/releasenotes/notes/add-reset-group-status-sd21a31cde5fa034.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added reset status API to generic volume group.