Add action extensions to support nova integration.

* Adds VolumeActions extension to support key functions
    described in blueprint volume-decoupling
 * Adds snapshot translations to APIs
 * Should be back in sync with 7992
 * blueprint compat-extensions
 * sleepsonthefloor: Moved added snapshot attributes to extension, added tests
 * sleepsonthefloor: Lock pep8==1.1 in tox.ini

Change-Id: I9c6118cd434ca8b275d2386546923f932420b975
This commit is contained in:
John Griffith 2012-06-13 12:38:35 -06:00 committed by Anthony Young
parent 2c6e273259
commit a9c8212f69
12 changed files with 523 additions and 44 deletions

View File

@ -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",

View File

@ -0,0 +1,125 @@
# 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.
"""The Extended Snapshot Attributes API extension."""
from webob import exc
from cinder.api.openstack import extensions
from cinder.api.openstack import wsgi
from cinder.api.openstack import xmlutil
from cinder import volume
from cinder import db
from cinder import exception
from cinder import flags
from cinder import log as logging
FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
authorize = extensions.soft_extension_authorizer('volume',
'extended_snapshot_attributes')
class ExtendedSnapshotAttributesController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(ExtendedSnapshotAttributesController, self).__init__(*args,
**kwargs)
self.volume_api = volume.API()
def _get_snapshots(self, context):
snapshots = self.volume_api.get_all_snapshots(context)
rval = dict((snapshot['id'], snapshot) for snapshot in snapshots)
return rval
def _extend_snapshot(self, context, snapshot, data):
for attr in ['project_id', 'progress']:
key = "%s:%s" % (Extended_snapshot_attributes.alias, attr)
snapshot[key] = data[attr]
@wsgi.extends
def show(self, req, resp_obj, id):
context = req.environ['cinder.context']
if authorize(context):
# Attach our slave template to the response object
resp_obj.attach(xml=ExtendedSnapshotAttributeTemplate())
try:
snapshot = self.volume_api.get_snapshot(context, id)
except exception.NotFound:
explanation = _("Snapshot not found.")
raise exc.HTTPNotFound(explanation=explanation)
self._extend_snapshot(context, resp_obj.obj['snapshot'], snapshot)
@wsgi.extends
def detail(self, req, resp_obj):
context = req.environ['cinder.context']
if authorize(context):
# Attach our slave template to the response object
resp_obj.attach(xml=ExtendedSnapshotAttributesTemplate())
snapshots = list(resp_obj.obj.get('snapshots', []))
db_snapshots = self._get_snapshots(context)
for snapshot_object in snapshots:
try:
snapshot_data = db_snapshots[snapshot_object['id']]
except KeyError:
continue
self._extend_snapshot(context, snapshot_object, snapshot_data)
class Extended_snapshot_attributes(extensions.ExtensionDescriptor):
"""Extended SnapshotAttributes support."""
name = "ExtendedSnapshotAttributes"
alias = "os-extended-snapshot-attributes"
namespace = ("http://docs.openstack.org/volume/ext/"
"extended_snapshot_attributes/api/v1")
updated = "2012-06-19T00:00:00+00:00"
def get_controller_extensions(self):
controller = ExtendedSnapshotAttributesController()
extension = extensions.ControllerExtension(self, 'snapshots',
controller)
return [extension]
def make_snapshot(elem):
elem.set('{%s}project_id' % Extended_snapshot_attributes.namespace,
'%s:project_id' % Extended_snapshot_attributes.alias)
elem.set('{%s}progress' % Extended_snapshot_attributes.namespace,
'%s:progress' % Extended_snapshot_attributes.alias)
class ExtendedSnapshotAttributeTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('snapshot', selector='snapshot')
make_snapshot(root)
alias = Extended_snapshot_attributes.alias
namespace = Extended_snapshot_attributes.namespace
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
class ExtendedSnapshotAttributesTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('snapshots')
elem = xmlutil.SubTemplateElement(root, 'snapshot',
selector='snapshots')
make_snapshot(elem)
alias = Extended_snapshot_attributes.alias
namespace = Extended_snapshot_attributes.namespace
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})

View 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]

View File

@ -33,28 +33,27 @@ LOG = logging.getLogger(__name__)
FLAGS = flags.FLAGS
def _translate_snapshot_detail_view(context, vol):
def _translate_snapshot_detail_view(context, snapshot):
"""Maps keys for snapshots details view."""
d = _translate_snapshot_summary_view(context, vol)
d = _translate_snapshot_summary_view(context, snapshot)
# NOTE(gagupta): No additional data / lookups at the moment
return d
def _translate_snapshot_summary_view(context, vol):
def _translate_snapshot_summary_view(context, snapshot):
"""Maps keys for snapshots summary view."""
d = {}
# TODO(bcwaldon): remove str cast once we use uuids
d['id'] = str(vol['id'])
d['volume_id'] = str(vol['volume_id'])
d['status'] = vol['status']
# NOTE(gagupta): We map volume_size as the snapshot size
d['size'] = vol['volume_size']
d['created_at'] = vol['created_at']
d['display_name'] = vol['display_name']
d['display_description'] = vol['display_description']
d['id'] = snapshot['id']
d['created_at'] = snapshot['created_at']
d['display_name'] = snapshot['display_name']
d['display_description'] = snapshot['display_description']
d['volume_id'] = snapshot['volume_id']
d['status'] = snapshot['status']
d['size'] = snapshot['volume_size']
return d

View File

@ -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',

View File

@ -0,0 +1,126 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# 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.
from lxml import etree
import webob
import json
from cinder.api.openstack.volume.contrib import extended_snapshot_attributes
from cinder import volume
from cinder import exception
from cinder import flags
from cinder import test
from cinder.tests.api.openstack import fakes
FLAGS = flags.FLAGS
UUID1 = '00000000-0000-0000-0000-000000000001'
UUID2 = '00000000-0000-0000-0000-000000000002'
def _get_default_snapshot_param():
return {
'id': UUID1,
'volume_id': 12,
'status': 'available',
'volume_size': 100,
'created_at': None,
'display_name': 'Default name',
'display_description': 'Default description',
'project_id': 'fake',
'progress': '0%'
}
def fake_snapshot_get(self, context, snapshot_id):
param = _get_default_snapshot_param()
return param
def fake_snapshot_get_all(self, context):
param = _get_default_snapshot_param()
return [param]
class ExtendedSnapshotAttributesTest(test.TestCase):
content_type = 'application/json'
prefix = 'os-extended-snapshot-attributes:'
def setUp(self):
super(ExtendedSnapshotAttributesTest, self).setUp()
self.stubs.Set(volume.api.API, 'get_snapshot', fake_snapshot_get)
self.stubs.Set(volume.api.API, 'get_all_snapshots',
fake_snapshot_get_all)
def _make_request(self, url):
req = webob.Request.blank(url)
req.headers['Accept'] = self.content_type
res = req.get_response(fakes.wsgi_app())
return res
def _get_snapshot(self, body):
return json.loads(body).get('snapshot')
def _get_snapshots(self, body):
return json.loads(body).get('snapshots')
def assertSnapshotAttributes(self, snapshot, project_id, progress):
self.assertEqual(snapshot.get('%sproject_id' % self.prefix),
project_id)
self.assertEqual(snapshot.get('%sprogress' % self.prefix), progress)
def test_show(self):
url = '/v1/fake/snapshots/%s' % UUID2
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
self.assertSnapshotAttributes(self._get_snapshot(res.body),
project_id='fake',
progress='0%')
def test_detail(self):
url = '/v1/fake/snapshots/detail'
res = self._make_request(url)
self.assertEqual(res.status_int, 200)
for i, snapshot in enumerate(self._get_snapshots(res.body)):
self.assertSnapshotAttributes(snapshot,
project_id='fake',
progress='0%')
def test_no_instance_passthrough_404(self):
def fake_snapshot_get(*args, **kwargs):
raise exception.InstanceNotFound()
self.stubs.Set(volume.api.API, 'get_snapshot', fake_snapshot_get)
url = '/v1/fake/snapshots/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
res = self._make_request(url)
self.assertEqual(res.status_int, 404)
class ExtendedSnapshotAttributesXmlTest(ExtendedSnapshotAttributesTest):
content_type = 'application/xml'
ext = extended_snapshot_attributes
prefix = '{%s}' % ext.Extended_snapshot_attributes.namespace
def _get_snapshot(self, body):
return etree.XML(body)
def _get_snapshots(self, body):
return etree.XML(body).getchildren()

View 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)

View File

@ -26,14 +26,17 @@ from cinder import test
from cinder import volume
from cinder.tests.api.openstack import fakes
FLAGS = flags.FLAGS
FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
UUID = '00000000-0000-0000-0000-000000000001'
INVALID_UUID = '00000000-0000-0000-0000-000000000002'
def _get_default_snapshot_param():
return {
'id': 123,
'id': UUID,
'volume_id': 12,
'status': 'available',
'volume_size': 100,
@ -52,12 +55,12 @@ def stub_snapshot_create(self, context, volume_id, name, description):
def stub_snapshot_delete(self, context, snapshot):
if snapshot['id'] != 123:
if snapshot['id'] != UUID:
raise exception.NotFound
def stub_snapshot_get(self, context, snapshot_id):
if snapshot_id != 123:
if snapshot_id != UUID:
raise exception.NotFound
param = _get_default_snapshot_param()
@ -116,30 +119,30 @@ class SnapshotApiTest(test.TestCase):
def test_snapshot_delete(self):
self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete)
snapshot_id = 123
req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id)
snapshot_id = UUID
req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % snapshot_id)
resp = self.controller.delete(req, snapshot_id)
self.assertEqual(resp.status_int, 202)
def test_snapshot_delete_invalid_id(self):
self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete)
snapshot_id = 234
req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id)
snapshot_id = INVALID_UUID
req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % snapshot_id)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete,
req,
snapshot_id)
def test_snapshot_show(self):
req = fakes.HTTPRequest.blank('/v1/snapshots/123')
resp_dict = self.controller.show(req, 123)
req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % UUID)
resp_dict = self.controller.show(req, UUID)
self.assertTrue('snapshot' in resp_dict)
self.assertEqual(resp_dict['snapshot']['id'], '123')
self.assertEqual(resp_dict['snapshot']['id'], UUID)
def test_snapshot_show_invalid_id(self):
snapshot_id = 234
req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id)
snapshot_id = INVALID_UUID
req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % snapshot_id)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req,
@ -154,7 +157,7 @@ class SnapshotApiTest(test.TestCase):
self.assertEqual(len(resp_snapshots), 1)
resp_snapshot = resp_snapshots.pop()
self.assertEqual(resp_snapshot['id'], '123')
self.assertEqual(resp_snapshot['id'], UUID)
class SnapshotSerializerTest(test.TestCase):

View File

@ -21,5 +21,6 @@
"volume:get_all_snapshots": [],
"volume_extension:types_manage": [],
"volume_extension:types_extra_specs": []
"volume_extension:types_extra_specs": [],
"volume_extension:extended_snapshot_attributes": []
}

View File

@ -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.

View File

@ -11,5 +11,6 @@
"volume:get_all_snapshots": [],
"volume_extension:types_manage": [["rule:admin_api"]],
"volume_extension:types_extra_specs": [["rule:admin_api"]]
"volume_extension:types_extra_specs": [["rule:admin_api"]],
"volume_extension:extended_snapshot_attributes": []
}

View File

@ -13,7 +13,7 @@ deps = -r{toxinidir}/tools/pip-requires
commands = /bin/bash run_tests.sh -N -P {posargs}
[testenv:pep8]
deps = pep8
deps = pep8==1.1
commands = pep8 --repeat --show-source cinder setup.py
[testenv:venv]