Added force-delete action to admin actions

force-delete action will be available
for shares and snapshots with admin creds.

Partially implements: blueprint manila-client-enhancements

Change-Id: I896e8a83109879b96f0d7ef2ca2e52fcb7528bb3
This commit is contained in:
Valeriy Ponomaryov 2014-06-03 08:23:13 -04:00
parent 3a67172141
commit cc0081fd10
8 changed files with 175 additions and 48 deletions

View File

@ -23,6 +23,7 @@ class AdminActionsTest(base.BaseSharesAdminTest):
def setUpClass(cls):
super(AdminActionsTest, cls).setUpClass()
cls.states = ["error", "available"]
cls.bad_status = "error_deleting"
__, cls.sh = cls.create_share()
__, cls.sn = cls.create_snapshot_wait_for_active(cls.sh["id"])
@ -42,3 +43,33 @@ class AdminActionsTest(base.BaseSharesAdminTest):
status=status)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.shares_client.wait_for_snapshot_status(self.sn["id"], status)
@test.attr(type=["gate", ])
def test_force_delete_share(self):
__, share = self.create_share()
# Change status from 'available' to 'error_deleting'
__, __ = self.shares_client.reset_state(share["id"],
status=self.bad_status)
# Check that status was changed
__, check_status = self.shares_client.get_share(share["id"])
self.assertEqual(check_status["status"], self.bad_status)
# Share with status 'error_deleting' should be deleted
resp, __ = self.shares_client.force_delete(share["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.shares_client.wait_for_resource_deletion(share_id=share["id"])
@test.attr(type=["gate", ])
def test_force_delete_snapshot(self):
__, sn = self.create_snapshot_wait_for_active(self.sh["id"])
# Change status from 'available' to 'error_deleting'
__, __ = self.shares_client.reset_state(sn["id"],
s_type="snapshots",
status=self.bad_status)
# Check that status was changed
__, check_status = self.shares_client.get_snapshot(sn["id"])
self.assertEqual(check_status["status"], self.bad_status)
# Snapshot with status 'error_deleting' should be deleted
resp, __ = self.shares_client.force_delete(sn["id"],
s_type="snapshots")
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.shares_client.wait_for_resource_deletion(snapshot_id=sn["id"])

View File

@ -32,12 +32,12 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
cls.member_shares_client = clients.Manager().shares_client
@test.attr(type=["gate", "negative", ])
def test_reset_unexistant_share_state(self):
def test_reset_nonexistent_share_state(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.reset_state, "fake")
@test.attr(type=["gate", "negative", ])
def test_reset_unexistant_snapshot_state(self):
def test_reset_nonexistent_snapshot_state(self):
self.assertRaises(exceptions.NotFound, self.shares_client.reset_state,
"fake", s_type="snapshots")
@ -66,3 +66,29 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.reset_state,
self.sn["id"], s_type="snapshots")
@test.attr(type=["gate", "negative", ])
def test_force_delete_nonexistent_share(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.force_delete, "fake")
@test.attr(type=["gate", "negative", ])
def test_force_delete_nonexistent_snapshot(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.force_delete,
"fake",
s_type="snapshots")
@test.attr(type=["gate", "negative", ])
def test_try_force_delete_share_with_member(self):
# If a non-admin tries to do force_delete, it should be unauthorized
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.force_delete,
self.sh["id"])
@test.attr(type=["gate", "negative", ])
def test_try_force_delete_snapshot_with_member(self):
# If a non-admin tries to do force_delete, it should be unauthorized
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.force_delete,
self.sn["id"], s_type="snapshots")

View File

@ -359,6 +359,15 @@ class SharesClient(rest_client.RestClient):
body = json.dumps(body)
return self.post("%s/%s/action" % (s_type, s_id), body)
def force_delete(self, s_id, s_type="shares"):
"""
Force delete share or snapshot
s_type: shares, snapshots
"""
body = {"os-force_delete": None}
body = json.dumps(body)
return self.post("%s/%s/action" % (s_type, s_id), body)
###############
def list_services(self, params=None):

View File

@ -15,7 +15,9 @@
"share_extension:quotas:update_for_user": [["rule:admin_or_projectadmin"]],
"share_extension:quota_classes": [],
"share_extension:share_admin_actions:force_delete": [["rule:admin_api"]],
"share_extension:share_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:snapshot_admin_actions:force_delete": [["rule:admin_api"]],
"share_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:services": [["rule:admin_api"]],

View File

@ -85,6 +85,18 @@ class AdminController(wsgi.Controller):
raise exc.HTTPNotFound(e)
return webob.Response(status_int=202)
@wsgi.action('os-force_delete')
def _force_delete(self, req, id, body):
"""Delete a resource, bypassing the check that it must be available."""
context = req.environ['manila.context']
self.authorize(context, 'force_delete')
try:
resource = self._get(context, id)
except exception.NotFound as e:
raise exc.HTTPNotFound(e)
self._delete(context, resource, force=True)
return webob.Response(status_int=202)
class ShareAdminController(AdminController):
"""AdminController for Shares."""

View File

@ -174,7 +174,7 @@ class API(base.Base):
return share
@policy.wrap_check_policy('share')
def delete(self, context, share):
def delete(self, context, share, force=False):
"""Delete share."""
if context.is_admin and context.project_id != share['project_id']:
project_id = share['project_id']
@ -197,7 +197,7 @@ class API(base.Base):
QUOTAS.commit(context, reservations, project_id=project_id)
return
if share['status'] not in ["available", "error"]:
if not (force or share['status'] in ["available", "error"]):
msg = _("Share status must be available or error")
raise exception.InvalidShare(reason=msg)
@ -276,8 +276,8 @@ class API(base.Base):
@policy.wrap_check_policy('share')
def delete_snapshot(self, context, snapshot, force=False):
if not force and snapshot['status'] not in ["available", "error"]:
msg = _("Share Snapshot status must be available or ")
if not (force or snapshot['status'] in ["available", "error"]):
msg = _("Share Snapshot status must be 'available' or 'error'.")
raise exception.InvalidShareSnapshot(reason=msg)
self.db.share_snapshot_update(context, snapshot['id'],

View File

@ -25,6 +25,7 @@ from manila import exception
from manila.openstack.common import jsonutils
from manila.share import api as share_api
from manila import test
from manila.tests.api.contrib import stubs
from manila.tests.api import fakes
CONF = cfg.CONF
@ -34,7 +35,7 @@ def app():
# no auth, just let environ['manila.context'] pass through
api = fakes.router.APIRouter()
mapper = fakes.urlmap.URLMap()
mapper['/v2'] = api
mapper['/v1'] = api
return mapper
@ -46,27 +47,27 @@ class AdminActionsTest(test.TestCase):
self.flags(rpc_backend='manila.openstack.common.rpc.impl_fake')
self.flags(lock_path=self.tempdir)
self.share_api = share_api.API()
self.admin_context = context.RequestContext('admin', 'fake', True)
self.member_context = context.RequestContext('fake', 'fake')
def tearDown(self):
shutil.rmtree(self.tempdir)
super(AdminActionsTest, self).tearDown()
def test_reset_status_as_admin(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# current status is available
share = db.share_create(ctx, {'status': 'available'})
req = webob.Request.blank('/v2/fake/shares/%s/action' % share['id'])
share = db.share_create(self.admin_context, {'status': 'available'})
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# request status of 'error'
req.body = jsonutils.dumps({'os-reset_status': {'status': 'error'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# request is accepted
self.assertEqual(resp.status_int, 202)
share = db.share_get(ctx, share['id'])
share = db.share_get(self.admin_context, share['id'])
# status changed to 'error'
self.assertEqual(share['status'], 'error')
@ -74,14 +75,14 @@ class AdminActionsTest(test.TestCase):
# current status is 'error'
share = db.share_create(context.get_admin_context(),
{'status': 'error'})
req = webob.Request.blank('/v2/fake/shares/%s/action' % share['id'])
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# request changing status to available
req.body = jsonutils.dumps({'os-reset_status': {'status':
'available'}})
# non-admin context
req.environ['manila.context'] = context.RequestContext('fake', 'fake')
req.environ['manila.context'] = self.member_context
resp = req.get_response(app())
# request is not authorized
self.assertEqual(resp.status_int, 403)
@ -90,48 +91,42 @@ class AdminActionsTest(test.TestCase):
self.assertEqual(share['status'], 'error')
def test_malformed_reset_status_body(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# current status is available
share = db.share_create(ctx, {'status': 'available'})
req = webob.Request.blank('/v2/fake/shares/%s/action' % share['id'])
share = db.share_create(self.admin_context, {'status': 'available'})
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# malformed request body
req.body = jsonutils.dumps({'os-reset_status': {'x-status': 'bad'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# bad request
self.assertEqual(resp.status_int, 400)
share = db.share_get(ctx, share['id'])
share = db.share_get(self.admin_context, share['id'])
# status is still 'available'
self.assertEqual(share['status'], 'available')
def test_invalid_status_for_share(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# current status is available
share = db.share_create(ctx, {'status': 'available'})
req = webob.Request.blank('/v2/fake/shares/%s/action' % share['id'])
share = db.share_create(self.admin_context, {'status': 'available'})
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# 'invalid' is not a valid status
req.body = jsonutils.dumps({'os-reset_status': {'status': 'invalid'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# bad request
self.assertEqual(resp.status_int, 400)
share = db.share_get(ctx, share['id'])
share = db.share_get(self.admin_context, share['id'])
# status is still 'available'
self.assertEqual(share['status'], 'available')
def test_reset_status_for_missing_share(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# missing-share-id
req = webob.Request.blank('/v2/fake/shares/%s/action' %
req = webob.Request.blank('/v1/fake/shares/%s/action' %
'missing-share-id')
req.method = 'POST'
req.headers['content-type'] = 'application/json'
@ -139,43 +134,41 @@ class AdminActionsTest(test.TestCase):
req.body = jsonutils.dumps({'os-reset_status': {'status':
'available'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# not found
self.assertEqual(resp.status_int, 404)
self.assertRaises(exception.NotFound, db.share_get, ctx,
self.assertRaises(exception.NotFound,
db.share_get,
self.admin_context,
'missing-share-id')
def test_snapshot_reset_status(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# snapshot in 'error_deleting'
share = db.share_create(ctx, {})
snapshot = db.share_snapshot_create(ctx, {'status': 'error_deleting',
'share_id': share['id']})
req = webob.Request.blank('/v2/fake/snapshots/%s/action' %
share = db.share_create(self.admin_context, {})
snapshot = db.share_snapshot_create(self.admin_context,
{'status': 'error_deleting', 'share_id': share['id']})
req = webob.Request.blank('/v1/fake/snapshots/%s/action' %
snapshot['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
# request status of 'error'
req.body = jsonutils.dumps({'os-reset_status': {'status': 'error'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# request is accepted
self.assertEqual(resp.status_int, 202)
snapshot = db.share_snapshot_get(ctx, snapshot['id'])
snapshot = db.share_snapshot_get(self.admin_context, snapshot['id'])
# status changed to 'error'
self.assertEqual(snapshot['status'], 'error')
def test_invalid_status_for_snapshot(self):
# admin context
ctx = context.RequestContext('admin', 'fake', True)
# snapshot in 'available'
share = db.share_create(ctx, {})
snapshot = db.share_snapshot_create(ctx, {'status': 'available',
'share_id': share['id']})
req = webob.Request.blank('/v2/fake/snapshots/%s/action' %
share = db.share_create(self.admin_context, {})
snapshot = db.share_snapshot_create(self.admin_context,
{'status': 'available', 'share_id': share['id']})
req = webob.Request.blank('/v1/fake/snapshots/%s/action' %
snapshot['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
@ -183,10 +176,62 @@ class AdminActionsTest(test.TestCase):
req.body = jsonutils.dumps({'os-reset_status': {'status':
'attaching'}})
# attach admin context to request
req.environ['manila.context'] = ctx
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
# request is accepted
self.assertEqual(resp.status_int, 400)
snapshot = db.share_snapshot_get(ctx, snapshot['id'])
snapshot = db.share_snapshot_get(self.admin_context, snapshot['id'])
# status is still 'available'
self.assertEqual(snapshot['status'], 'available')
def test_admin_force_delete_share(self):
share = db.share_create(self.admin_context, {'size': 1})
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.body = jsonutils.dumps({'os-force_delete': {}})
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
self.assertEqual(resp.status_int, 202)
self.assertRaises(exception.NotFound,
db.share_get,
self.admin_context,
share['id'])
def test_member_force_delete_share(self):
share = db.share_create(self.admin_context, {'size': 1})
req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id'])
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.body = jsonutils.dumps({'os-force_delete': {}})
req.environ['manila.context'] = self.member_context
resp = req.get_response(app())
self.assertEqual(resp.status_int, 403)
def test_admin_force_delete_snapshot(self):
snapshot = stubs.stub_snapshot(1, host='foo')
self.stubs.Set(db, 'share_get', lambda x, y: snapshot)
self.stubs.Set(db, 'share_snapshot_get', lambda x, y: snapshot)
self.stubs.Set(share_api.API, 'delete_snapshot', lambda *x, **y: True)
path = '/v1/fake/snapshots/%s/action' % snapshot['id']
req = webob.Request.blank(path)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.body = jsonutils.dumps({'os-force_delete': {}})
req.environ['manila.context'] = self.admin_context
resp = req.get_response(app())
self.assertEqual(resp.status_int, 202)
def test_member_force_delete_snapshot(self):
snapshot = stubs.stub_snapshot(1, host='foo')
self.stubs.Set(db, 'share_get', lambda x, y: snapshot)
self.stubs.Set(db, 'share_snapshot_get', lambda x, y: snapshot)
self.stubs.Set(share_api.API, 'delete_snapshot', lambda *x, **y: True)
path = '/v1/fake/snapshots/%s/action' % snapshot['id']
req = webob.Request.blank(path)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.body = jsonutils.dumps({'os-force_delete': {}})
req.environ['manila.context'] = self.member_context
resp = req.get_response(app())
self.assertEqual(resp.status_int, 403)

View File

@ -20,7 +20,9 @@
"share:get_share_metadata": [],
"share:delete_share_metadata": [],
"share:update_share_metadata": [],
"share_extension:share_admin_actions:force_delete": [["rule:admin_api"]],
"share_extension:share_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:snapshot_admin_actions:force_delete": [["rule:admin_api"]],
"share_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:types_manage": [],
"share_extension:types_extra_specs": [],