From 15c555445bb61ec856ce3b0b9d3fb90df00d349f Mon Sep 17 00:00:00 2001 From: TommyLike Date: Thu, 20 Oct 2016 17:30:31 +0800 Subject: [PATCH] [1/4]Reset generic volume group status Currently the administrator could only reset the generic group status by db operation,this change intends to add new admin actions to achieve these. The patch list: 1. group API(this). 2. group snapshot API(https://review.openstack.org/#/c/389577/). 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: Ib8bffb806f878c67bb12fd5ef7ed8cc15606d1c5 --- cinder/api/openstack/api_version_request.py | 4 +- .../openstack/rest_api_version_history.rst | 4 ++ cinder/api/v3/groups.py | 42 +++++++++++++++++++ cinder/exception.py | 4 ++ cinder/group/api.py | 14 +++++++ cinder/tests/functional/api/client.py | 3 ++ cinder/tests/functional/test_groups.py | 16 ++++++- cinder/tests/unit/api/v3/test_groups.py | 41 ++++++++++++++++++ cinder/tests/unit/group/test_groups_api.py | 12 ++++++ cinder/tests/unit/policy.json | 1 + etc/cinder/policy.json | 1 + ...d-reset-group-status-sd21a31cde5fa034.yaml | 3 ++ 12 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-reset-group-status-sd21a31cde5fa034.yaml 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.