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.17 - Getting manageable volumes and snapshots now accepts cluster.
|
||||||
* 3.18 - Add backup project attribute.
|
* 3.18 - Add backup project attribute.
|
||||||
* 3.19 - Add API reset status actions 'reset_status' to group snapshot.
|
* 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
|
# The minimum and maximum versions of the API supported
|
||||||
@@ -75,7 +77,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v1 or /v2 enpoints will still work
|
# Explicitly using /v1 or /v2 enpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.19"
|
_MAX_API_VERSION = "3.20"
|
||||||
_LEGACY_API_VERSION1 = "1.0"
|
_LEGACY_API_VERSION1 = "1.0"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
|
|
||||||
|
|||||||
@@ -214,3 +214,7 @@ user documentation.
|
|||||||
3.19
|
3.19
|
||||||
----
|
----
|
||||||
Added reset status actions 'reset_status' to group snapshot.
|
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 exception
|
||||||
from cinder import group as group_api
|
from cinder import group as group_api
|
||||||
from cinder.i18n import _, _LI
|
from cinder.i18n import _, _LI
|
||||||
|
from cinder import rpc
|
||||||
from cinder.volume import group_types
|
from cinder.volume import group_types
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -65,6 +66,47 @@ class GroupsController(wsgi.Controller):
|
|||||||
|
|
||||||
return self._view_builder.detail(req, group)
|
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.Controller.api_version(GROUP_API_VERSION)
|
||||||
@wsgi.action("delete")
|
@wsgi.action("delete")
|
||||||
def delete_group(self, req, id, body):
|
def delete_group(self, req, id, body):
|
||||||
|
|||||||
@@ -1061,6 +1061,10 @@ class InvalidGroup(Invalid):
|
|||||||
message = _("Invalid Group: %(reason)s")
|
message = _("Invalid Group: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidGroupStatus(Invalid):
|
||||||
|
message = _("Invalid Group Status: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
# CgSnapshot
|
# CgSnapshot
|
||||||
class CgSnapshotNotFound(NotFound):
|
class CgSnapshotNotFound(NotFound):
|
||||||
message = _("CgSnapshot %(cgsnapshot_id)s could not be found.")
|
message = _("CgSnapshot %(cgsnapshot_id)s could not be found.")
|
||||||
|
|||||||
@@ -772,6 +772,20 @@ class API(base.Base):
|
|||||||
sort_dirs=sort_dirs)
|
sort_dirs=sort_dirs)
|
||||||
return groups
|
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):
|
def create_group_snapshot(self, context, group, name, description):
|
||||||
options = {'group_id': group.id,
|
options = {'group_id': group.id,
|
||||||
'user_id': context.user_id,
|
'user_id': context.user_id,
|
||||||
|
|||||||
@@ -253,6 +253,9 @@ class TestOpenStackClient(object):
|
|||||||
def delete_group(self, group_id, params):
|
def delete_group(self, group_id, params):
|
||||||
return self.api_post('/groups/%s/action' % 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):
|
def put_group(self, group_id, group):
|
||||||
return self.api_put('/groups/%s' % group_id, group)['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'
|
_vol_type_name = 'functional_test_type'
|
||||||
_grp_type_name = 'functional_grp_test_type'
|
_grp_type_name = 'functional_grp_test_type'
|
||||||
osapi_version_major = '3'
|
osapi_version_major = '3'
|
||||||
osapi_version_minor = '13'
|
osapi_version_minor = '20'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(GroupsTest, self).setUp()
|
super(GroupsTest, self).setUp()
|
||||||
self.volume_type = self.api.create_type(self._vol_type_name)
|
self.volume_type = self.api.create_type(self._vol_type_name)
|
||||||
self.group_type = self.api.create_group_type(self._grp_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):
|
def _get_flags(self):
|
||||||
f = super(GroupsTest, self)._get_flags()
|
f = super(GroupsTest, self)._get_flags()
|
||||||
@@ -45,6 +48,17 @@ class GroupsTest(functional_helpers._FunctionalTestBase):
|
|||||||
grps = self.api.get_groups()
|
grps = self.api.get_groups()
|
||||||
self.assertIsNotNone(grps)
|
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):
|
def test_create_and_delete_group(self):
|
||||||
"""Creates and deletes a group."""
|
"""Creates and deletes a group."""
|
||||||
|
|
||||||
|
|||||||
@@ -824,6 +824,47 @@ class GroupsAPITestCase(test.TestCase):
|
|||||||
self.controller.update,
|
self.controller.update,
|
||||||
req, self.group1.id, body)
|
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(
|
@mock.patch(
|
||||||
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
|
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
|
||||||
def test_create_group_from_src_snap(self, mock_validate):
|
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,
|
mock_volume_types_get.assert_called_once_with(mock.ANY,
|
||||||
volume_type_names)
|
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.object(GROUP_QUOTAS, "reserve")
|
||||||
@mock.patch('cinder.objects.Group')
|
@mock.patch('cinder.objects.Group')
|
||||||
@mock.patch('cinder.db.group_type_get_by_name')
|
@mock.patch('cinder.db.group_type_get_by_name')
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"group:get_group_snapshot": "",
|
"group:get_group_snapshot": "",
|
||||||
"group:get_all_group_snapshots": "",
|
"group:get_all_group_snapshots": "",
|
||||||
"group:reset_group_snapshot_status":"",
|
"group:reset_group_snapshot_status":"",
|
||||||
|
"group:reset_status":"",
|
||||||
|
|
||||||
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
|
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
"group:get_group_snapshot": "rule:admin_or_owner",
|
"group:get_group_snapshot": "rule:admin_or_owner",
|
||||||
"group:get_all_group_snapshots": "rule:admin_or_owner",
|
"group:get_all_group_snapshots": "rule:admin_or_owner",
|
||||||
"group:reset_group_snapshot_status":"rule:admin_api",
|
"group:reset_group_snapshot_status":"rule:admin_api",
|
||||||
|
"group:reset_status":"rule:admin_api",
|
||||||
|
|
||||||
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
|
"scheduler_extension:scheduler_stats:get_pools" : "rule:admin_api",
|
||||||
"message:delete": "rule:admin_or_owner",
|
"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