Skip to content

Commit

Permalink
Merge "Add admin actions extension"
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Sep 6, 2012
2 parents 802c05b + c191d0d commit 5d72e7a
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 17 deletions.
5 changes: 3 additions & 2 deletions cinder/api/openstack/extensions.py
Expand Up @@ -216,11 +216,12 @@ def get_controller_extensions(self):
controller_exts = []
for ext in self.extensions.values():
try:
controller_exts.extend(ext.get_controller_extensions())
get_ext_method = ext.get_controller_extensions
except AttributeError:
# NOTE(Vek): Extensions aren't required to have
# controller extensions
pass
continue
controller_exts.extend(get_ext_method())
return controller_exts

def _check_extension(self, extension):
Expand Down
5 changes: 3 additions & 2 deletions cinder/api/openstack/volume/__init__.py
Expand Up @@ -58,10 +58,11 @@ def _setup_routes(self, mapper, ext_mgr):
mapper.resource("type", "types",
controller=self.resources['types'])

self.resources['snapshots'] = snapshots.create_resource()
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
mapper.resource("snapshot", "snapshots",
controller=self.resources['snapshots'],
collection={'detail': 'GET'})
collection={'detail': 'GET'},
member={'action': 'POST'})

self.resources['limits'] = limits.create_resource()
mapper.resource("limit", "limits",
Expand Down
129 changes: 129 additions & 0 deletions cinder/api/openstack/volume/contrib/admin_actions.py
@@ -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
7 changes: 4 additions & 3 deletions cinder/api/openstack/volume/snapshots.py
Expand Up @@ -86,8 +86,9 @@ def construct(self):
class SnapshotsController(object):
"""The Volumes API controller for the OpenStack API."""

def __init__(self):
def __init__(self, ext_mgr=None):
self.volume_api = volume.API()
self.ext_mgr = ext_mgr
super(SnapshotsController, self).__init__()

@wsgi.serializers(xml=SnapshotTemplate)
Expand Down Expand Up @@ -169,5 +170,5 @@ def create(self, req, body):
return {'snapshot': retval}


def create_resource():
return wsgi.Resource(SnapshotsController())
def create_resource(ext_mgr):
return wsgi.Resource(SnapshotsController(ext_mgr))
3 changes: 3 additions & 0 deletions cinder/api/openstack/wsgi.py
Expand Up @@ -969,6 +969,9 @@ def __new__(mcs, name, bases, cls_dict):
# Find all actions
actions = {}
extensions = []
# start with wsgi actions from base classes
for base in bases:
actions.update(getattr(base, 'wsgi_actions', {}))
for key, value in cls_dict.items():
if not callable(value):
continue
Expand Down
176 changes: 176 additions & 0 deletions cinder/tests/api/openstack/volume/contrib/test_admin_actions.py
@@ -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'])
8 changes: 2 additions & 6 deletions cinder/tests/api/openstack/volume/test_router.py
Expand Up @@ -40,11 +40,7 @@ def detail(self, req):
return {}


def create_resource():
return wsgi.Resource(FakeController())


def create_volume_resource(ext_mgr):
def create_resource(ext_mgr):
return wsgi.Resource(FakeController(ext_mgr))


Expand All @@ -53,7 +49,7 @@ def setUp(self):
super(VolumeRouterTestCase, self).setUp()
# NOTE(vish): versions is just returning text so, no need to stub.
self.stubs.Set(snapshots, 'create_resource', create_resource)
self.stubs.Set(volumes, 'create_resource', create_volume_resource)
self.stubs.Set(volumes, 'create_resource', create_resource)
self.app = volume.APIRouter()

def test_versions(self):
Expand Down

0 comments on commit 5d72e7a

Please sign in to comment.