Merge "[1/4]Reset generic volume group status"
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added reset status API to generic volume group.
|
||||
Reference in New Issue
Block a user