Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
377 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# Copyright 2012 OpenStack, LLC. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
# not use this file except in compliance with the License. You may obtain | ||
# a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
# License for the specific language governing permissions and limitations | ||
# under the License. | ||
|
||
import webob | ||
from webob import exc | ||
|
||
from cinder.api.openstack import extensions | ||
from cinder.api.openstack import wsgi | ||
from cinder import db | ||
from cinder import exception | ||
from cinder import volume | ||
from cinder.openstack.common import log as logging | ||
|
||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class AdminController(wsgi.Controller): | ||
"""Abstract base class for AdminControllers.""" | ||
|
||
collection = None # api collection to extend | ||
|
||
# FIXME(clayg): this will be hard to keep up-to-date | ||
# Concrete classes can expand or over-ride | ||
valid_status = set([ | ||
'creating', | ||
'available', | ||
'deleting', | ||
'error', | ||
'error_deleting', | ||
]) | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(AdminController, self).__init__(*args, **kwargs) | ||
# singular name of the resource | ||
self.resource_name = self.collection.rstrip('s') | ||
self.volume_api = volume.API() | ||
|
||
def _update(self, *args, **kwargs): | ||
raise NotImplementedError() | ||
|
||
def _validate_status(self, status): | ||
if status not in self.valid_status: | ||
raise exc.HTTPBadRequest("Must specify a valid status") | ||
|
||
def authorize(self, context, action_name): | ||
# e.g. "snapshot_admin_actions:reset_status" | ||
action = '%s_admin_actions:%s' % (self.resource_name, action_name) | ||
extensions.extension_authorizer('volume', action)(context) | ||
|
||
@wsgi.action('os-reset_status') | ||
def _reset_status(self, req, id, body): | ||
"""Reset status on the resource.""" | ||
context = req.environ['cinder.context'] | ||
self.authorize(context, 'reset_status') | ||
try: | ||
new_status = body['os-reset_status']['status'] | ||
except (TypeError, KeyError): | ||
raise exc.HTTPBadRequest("Must specify 'status'") | ||
self._validate_status(new_status) | ||
msg = _("Updating status of %(resource)s '%(id)s' to '%(status)s'") | ||
LOG.debug(msg, {'resource': self.resource_name, 'id': id, | ||
'status': new_status}) | ||
try: | ||
self._update(context, id, {'status': new_status}) | ||
except exception.NotFound, e: | ||
raise exc.HTTPNotFound(e) | ||
return webob.Response(status_int=202) | ||
|
||
|
||
class VolumeAdminController(AdminController): | ||
"""AdminController for Volumes.""" | ||
|
||
collection = 'volumes' | ||
valid_status = AdminController.valid_status.union( | ||
set(['attaching', 'in-use', 'detaching'])) | ||
|
||
def _update(self, *args, **kwargs): | ||
db.volume_update(*args, **kwargs) | ||
|
||
@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['cinder.context'] | ||
self.authorize(context, 'force_delete') | ||
try: | ||
volume = self.volume_api.get(context, id) | ||
except exception.NotFound: | ||
raise exc.HTTPNotFound() | ||
self.volume_api.delete(context, volume, force=True) | ||
return webob.Response(status_int=202) | ||
|
||
|
||
class SnapshotAdminController(AdminController): | ||
"""AdminController for Snapshots.""" | ||
|
||
collection = 'snapshots' | ||
|
||
def _update(self, *args, **kwargs): | ||
db.snapshot_update(*args, **kwargs) | ||
|
||
|
||
class Admin_actions(extensions.ExtensionDescriptor): | ||
"""Enable admin actions.""" | ||
|
||
name = "AdminActions" | ||
alias = "os-admin-actions" | ||
namespace = "http://docs.openstack.org/volume/ext/admin-actions/api/v1.1" | ||
updated = "2012-08-25T00:00:00+00:00" | ||
|
||
def get_controller_extensions(self): | ||
exts = [] | ||
for class_ in (VolumeAdminController, SnapshotAdminController): | ||
controller = class_() | ||
extension = extensions.ControllerExtension( | ||
self, class_.collection, controller) | ||
exts.append(extension) | ||
return exts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
cinder/tests/api/openstack/volume/contrib/test_admin_actions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import webob | ||
|
||
from cinder import context | ||
from cinder import db | ||
from cinder import exception | ||
from cinder import test | ||
from cinder.openstack.common import jsonutils | ||
from cinder.tests.api.openstack import fakes | ||
|
||
|
||
def app(): | ||
# no auth, just let environ['cinder.context'] pass through | ||
api = fakes.volume.APIRouter() | ||
mapper = fakes.urlmap.URLMap() | ||
mapper['/v1'] = api | ||
return mapper | ||
|
||
|
||
class AdminActionsTest(test.TestCase): | ||
|
||
def test_reset_status_as_admin(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# current status is available | ||
volume = db.volume_create(ctx, {'status': 'available'}) | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['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['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# request is accepted | ||
self.assertEquals(resp.status_int, 202) | ||
volume = db.volume_get(ctx, volume['id']) | ||
# status changed to 'error' | ||
self.assertEquals(volume['status'], 'error') | ||
|
||
def test_reset_status_as_non_admin(self): | ||
# current status is 'error' | ||
volume = db.volume_create(context.get_admin_context(), | ||
{'status': 'error'}) | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['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['cinder.context'] = context.RequestContext('fake', 'fake') | ||
resp = req.get_response(app()) | ||
# request is not authorized | ||
self.assertEquals(resp.status_int, 403) | ||
volume = db.volume_get(context.get_admin_context(), volume['id']) | ||
# status is still 'error' | ||
self.assertEquals(volume['status'], 'error') | ||
|
||
def test_malformed_reset_status_body(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# current status is available | ||
volume = db.volume_create(ctx, {'status': 'available'}) | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['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['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# bad request | ||
self.assertEquals(resp.status_int, 400) | ||
volume = db.volume_get(ctx, volume['id']) | ||
# status is still 'available' | ||
self.assertEquals(volume['status'], 'available') | ||
|
||
def test_invalid_status_for_volume(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# current status is available | ||
volume = db.volume_create(ctx, {'status': 'available'}) | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['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['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# bad request | ||
self.assertEquals(resp.status_int, 400) | ||
volume = db.volume_get(ctx, volume['id']) | ||
# status is still 'available' | ||
self.assertEquals(volume['status'], 'available') | ||
|
||
def test_reset_status_for_missing_volume(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# missing-volume-id | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % | ||
'missing-volume-id') | ||
req.method = 'POST' | ||
req.headers['content-type'] = 'application/json' | ||
# malformed request body | ||
req.body = jsonutils.dumps({'os-reset_status': {'status': | ||
'available'}}) | ||
# attach admin context to request | ||
req.environ['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# not found | ||
self.assertEquals(resp.status_int, 404) | ||
self.assertRaises(exception.NotFound, db.volume_get, ctx, | ||
'missing-volume-id') | ||
|
||
def test_snapshot_reset_status(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# snapshot in 'error_deleting' | ||
volume = db.volume_create(ctx, {}) | ||
snapshot = db.snapshot_create(ctx, {'status': 'error_deleting', | ||
'volume_id': volume['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['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# request is accepted | ||
self.assertEquals(resp.status_int, 202) | ||
snapshot = db.snapshot_get(ctx, snapshot['id']) | ||
# status changed to 'error' | ||
self.assertEquals(snapshot['status'], 'error') | ||
|
||
def test_invalid_status_for_snapshot(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# snapshot in 'available' | ||
volume = db.volume_create(ctx, {}) | ||
snapshot = db.snapshot_create(ctx, {'status': 'available', | ||
'volume_id': volume['id']}) | ||
req = webob.Request.blank('/v1/fake/snapshots/%s/action' % | ||
snapshot['id']) | ||
req.method = 'POST' | ||
req.headers['content-type'] = 'application/json' | ||
# 'attaching' is not a valid status for snapshots | ||
req.body = jsonutils.dumps({'os-reset_status': {'status': | ||
'attaching'}}) | ||
# attach admin context to request | ||
req.environ['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# request is accepted | ||
self.assertEquals(resp.status_int, 400) | ||
snapshot = db.snapshot_get(ctx, snapshot['id']) | ||
# status is still 'available' | ||
self.assertEquals(snapshot['status'], 'available') | ||
|
||
def test_force_delete(self): | ||
# admin context | ||
ctx = context.RequestContext('admin', 'fake', True) | ||
# current status is creating | ||
volume = db.volume_create(ctx, {'status': 'creating'}) | ||
req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) | ||
req.method = 'POST' | ||
req.headers['content-type'] = 'application/json' | ||
req.body = jsonutils.dumps({'os-force_delete': {}}) | ||
# attach admin context to request | ||
req.environ['cinder.context'] = ctx | ||
resp = req.get_response(app()) | ||
# request is accepted | ||
self.assertEquals(resp.status_int, 202) | ||
# volume is deleted | ||
self.assertRaises(exception.NotFound, db.volume_get, ctx, volume['id']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.