Merge "Add action extensions to support nova integration."
This commit is contained in:
@@ -50,7 +50,8 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
||||
self.resources['volumes'] = volumes.create_resource()
|
||||
mapper.resource("volume", "volumes",
|
||||
controller=self.resources['volumes'],
|
||||
collection={'detail': 'GET'})
|
||||
collection={'detail': 'GET'},
|
||||
member={'action': 'POST'})
|
||||
|
||||
self.resources['types'] = types.create_resource()
|
||||
mapper.resource("type", "types",
|
||||
|
||||
114
cinder/api/openstack/volume/contrib/volume_actions.py
Normal file
114
cinder/api/openstack/volume/contrib/volume_actions.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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 os.path
|
||||
import traceback
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from cinder.api.openstack import common
|
||||
from cinder.api.openstack import extensions
|
||||
from cinder.api.openstack import wsgi
|
||||
from cinder import volume
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder import log as logging
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def authorize(context, action_name):
|
||||
action = 'volume_actions:%s' % action_name
|
||||
extensions.extension_authorizer('volume', action)(context)
|
||||
|
||||
|
||||
class VolumeActionsController(wsgi.Controller):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VolumeActionsController, self).__init__(*args, **kwargs)
|
||||
self.volume_api = volume.API()
|
||||
|
||||
@wsgi.action('os-attach')
|
||||
def _attach(self, req, id, body):
|
||||
"""Add attachment metadata."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
|
||||
instance_uuid = body['os-attach']['instance_uuid']
|
||||
mountpoint = body['os-attach']['mountpoint']
|
||||
|
||||
self.volume_api.attach(context, volume,
|
||||
instance_uuid, mountpoint)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('os-detach')
|
||||
def _detach(self, req, id, body):
|
||||
"""Clear attachment metadata."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
self.volume_api.detach(context, volume)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('os-reserve')
|
||||
def _reserve(self, req, id, body):
|
||||
"""Mark volume as reserved."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
self.volume_api.reserve_volume(context, volume)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('os-unreserve')
|
||||
def _unreserve(self, req, id, body):
|
||||
"""Unmark volume as reserved."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
self.volume_api.unreserve_volume(context, volume)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('os-initialize_connection')
|
||||
def _initialize_connection(self, req, id, body):
|
||||
"""Initialize volume attachment."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
connector = body['os-initialize_connection']['connector']
|
||||
info = self.volume_api.initialize_connection(context,
|
||||
volume,
|
||||
connector)
|
||||
return {'connection_info': info}
|
||||
|
||||
@wsgi.action('os-terminate_connection')
|
||||
def _terminate_connection(self, req, id, body):
|
||||
"""Terminate volume attachment."""
|
||||
context = req.environ['cinder.context']
|
||||
volume = self.volume_api.get(context, id)
|
||||
connector = body['os-terminate_connection']['connector']
|
||||
self.volume_api.terminate_connection(context, volume, connector)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
class Volume_actions(extensions.ExtensionDescriptor):
|
||||
"""Enable volume actions
|
||||
"""
|
||||
|
||||
name = "VolumeActions"
|
||||
alias = "os-volume-actions"
|
||||
namespace = "http://docs.openstack.org/volume/ext/volume-actions/api/v1.1"
|
||||
updated = "2012-05-31T00:00:00+00:00"
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = VolumeActionsController()
|
||||
extension = extensions.ControllerExtension(self, 'volumes', controller)
|
||||
return [extension]
|
||||
@@ -26,6 +26,7 @@ from cinder.api import auth as api_auth
|
||||
from cinder.api import openstack as openstack_api
|
||||
from cinder.api.openstack import auth
|
||||
from cinder.api.openstack import urlmap
|
||||
from cinder.api.openstack import volume
|
||||
from cinder.api.openstack.volume import versions
|
||||
from cinder.api.openstack import wsgi as os_wsgi
|
||||
from cinder import context
|
||||
@@ -60,28 +61,27 @@ def fake_wsgi(self, req):
|
||||
return self.application
|
||||
|
||||
|
||||
def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None,
|
||||
def wsgi_app(inner_app_v1=None, fake_auth=True, fake_auth_context=None,
|
||||
use_no_auth=False, ext_mgr=None):
|
||||
if not inner_app_v2:
|
||||
inner_app_v2 = compute.APIRouter(ext_mgr)
|
||||
if not inner_app_v1:
|
||||
inner_app_v1 = volume.APIRouter(ext_mgr)
|
||||
|
||||
if fake_auth:
|
||||
if fake_auth_context is not None:
|
||||
ctxt = fake_auth_context
|
||||
else:
|
||||
ctxt = context.RequestContext('fake', 'fake', auth_token=True)
|
||||
api_v2 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
|
||||
limits.RateLimitingMiddleware(inner_app_v2)))
|
||||
api_v1 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
|
||||
inner_app_v1))
|
||||
elif use_no_auth:
|
||||
api_v2 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
|
||||
limits.RateLimitingMiddleware(inner_app_v2)))
|
||||
api_v1 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
|
||||
limits.RateLimitingMiddleware(inner_app_v1)))
|
||||
else:
|
||||
api_v2 = openstack_api.FaultWrapper(auth.AuthMiddleware(
|
||||
limits.RateLimitingMiddleware(inner_app_v2)))
|
||||
api_v1 = openstack_api.FaultWrapper(auth.AuthMiddleware(
|
||||
limits.RateLimitingMiddleware(inner_app_v1)))
|
||||
|
||||
mapper = urlmap.URLMap()
|
||||
mapper['/v2'] = api_v2
|
||||
mapper['/v1.1'] = api_v2
|
||||
mapper['/v1'] = api_v1
|
||||
mapper['/'] = openstack_api.FaultWrapper(versions.Versions())
|
||||
return mapper
|
||||
|
||||
@@ -122,7 +122,7 @@ class HTTPRequest(webob.Request):
|
||||
|
||||
@classmethod
|
||||
def blank(cls, *args, **kwargs):
|
||||
kwargs['base_url'] = 'http://localhost/v2'
|
||||
kwargs['base_url'] = 'http://localhost/v1'
|
||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||
out = webob.Request.blank(*args, **kwargs)
|
||||
out.environ['cinder.context'] = FakeRequestContext('fake_user', 'fake',
|
||||
|
||||
107
cinder/tests/api/openstack/volume/contrib/test_volume_actions.py
Normal file
107
cinder/tests/api/openstack/volume/contrib/test_volume_actions.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# 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 datetime
|
||||
import json
|
||||
|
||||
import webob
|
||||
|
||||
from cinder.api.openstack import volume as volume_api
|
||||
from cinder import volume
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder import test
|
||||
from cinder.tests.api.openstack import fakes
|
||||
from cinder import utils
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def fake_volume_api(*args, **kwargs):
|
||||
return True
|
||||
|
||||
|
||||
def fake_volume_get(*args, **kwargs):
|
||||
return {'id': 'fake', 'host': 'fake'}
|
||||
|
||||
|
||||
class VolumeActionsTest(test.TestCase):
|
||||
|
||||
_actions = ('os-detach', 'os-reserve', 'os-unreserve')
|
||||
|
||||
_methods = ('attach', 'detach', 'reserve_volume', 'unreserve_volume')
|
||||
|
||||
def setUp(self):
|
||||
super(VolumeActionsTest, self).setUp()
|
||||
self.stubs.Set(volume.API, 'get', fake_volume_api)
|
||||
self.UUID = utils.gen_uuid()
|
||||
for _method in self._methods:
|
||||
self.stubs.Set(volume.API, _method, fake_volume_api)
|
||||
|
||||
self.stubs.Set(volume.API, 'get', fake_volume_get)
|
||||
|
||||
def test_simple_api_actions(self):
|
||||
app = fakes.wsgi_app()
|
||||
for _action in self._actions:
|
||||
req = webob.Request.blank('/v1/fake/volumes/%s/action' %
|
||||
self.UUID)
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps({_action: None})
|
||||
req.content_type = 'application/json'
|
||||
res = req.get_response(app)
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
def fake_initialize_connection(*args, **kwargs):
|
||||
return {}
|
||||
self.stubs.Set(volume.API, 'initialize_connection',
|
||||
fake_initialize_connection)
|
||||
|
||||
body = {'os-initialize_connection': {'connector': 'fake'}}
|
||||
req = webob.Request.blank('/v1/fake/volumes/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
output = json.loads(res.body)
|
||||
self.assertEqual(res.status_int, 200)
|
||||
|
||||
def test_terminate_connection(self):
|
||||
def fake_terminate_connection(*args, **kwargs):
|
||||
return {}
|
||||
self.stubs.Set(volume.API, 'terminate_connection',
|
||||
fake_terminate_connection)
|
||||
|
||||
body = {'os-terminate_connection': {'connector': 'fake'}}
|
||||
req = webob.Request.blank('/v1/fake/volumes/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_attach(self):
|
||||
body = {'os-attach': {'instance_uuid': 'fake',
|
||||
'mountpoint': '/dev/vdc'}}
|
||||
req = webob.Request.blank('/v1/fake/volumes/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
@@ -230,10 +230,11 @@ class VolumeManager(manager.SchedulerDependentManager):
|
||||
def attach_volume(self, context, volume_id, instance_uuid, mountpoint):
|
||||
"""Updates db to show volume is attached"""
|
||||
# TODO(vish): refactor this into a more general "reserve"
|
||||
# TODO(sleepsonthefloor): Is this 'elevated' appropriate?
|
||||
if not utils.is_uuid_like(instance_uuid):
|
||||
raise exception.InvalidUUID(instance_uuid)
|
||||
|
||||
self.db.volume_attached(context,
|
||||
self.db.volume_attached(context.elevated(),
|
||||
volume_id,
|
||||
instance_uuid,
|
||||
mountpoint)
|
||||
@@ -241,7 +242,8 @@ class VolumeManager(manager.SchedulerDependentManager):
|
||||
def detach_volume(self, context, volume_id):
|
||||
"""Updates db to show volume is detached"""
|
||||
# TODO(vish): refactor this into a more general "unreserve"
|
||||
self.db.volume_detached(context, volume_id)
|
||||
# TODO(sleepsonthefloor): Is this 'elevated' appropriate?
|
||||
self.db.volume_detached(context.elevated(), volume_id)
|
||||
|
||||
def initialize_connection(self, context, volume_id, connector):
|
||||
"""Prepare volume for connection from host represented by connector.
|
||||
|
||||
Reference in New Issue
Block a user