Add os-assisted-volume-snapshots extension

Add a new API extension that exposes assisted volume snapshot
capabilities.  This extension is admin only by default.  We expect it to
only be called by Cinder.  If you have your deployment set up in such a
way that your adminURL is different from the public, this extension can
only be loaded in the admin API instance.  Cinder will pull that URL out
of the service catalog to use.

Part of blueprint qemu-assisted-snapshots

Change-Id: I79e22ab6ef66fa16dc534a4336e766065702b2f5
This commit is contained in:
Russell Bryant 2013-08-16 20:44:37 +00:00
parent 87b3d3a6f0
commit fa13644b05
19 changed files with 315 additions and 13 deletions

View File

@ -128,6 +128,14 @@
"namespace": "http://docs.openstack.org/compute/ext/aggregates/api/v1.1",
"updated": "2012-01-12T00:00:00+00:00"
},
{
"alias": "os-assisted-volume-snapshots",
"description": "Assisted volume snapshots.",
"links": [],
"name": "AssistedVolumeSnapshots",
"namespace": "http://docs.openstack.org/compute/ext/assisted-volume-snapshots/api/v2",
"updated": "2013-08-15T00:00:00-00:00"
},
{
"alias": "os-attach-interfaces",
"description": "Attach interface support.",
@ -472,14 +480,6 @@
"namespace": "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1",
"updated": "2011-08-08T00:00:00+00:00"
},
{
"alias": "os-user-quotas",
"description": "Project user quota support.",
"links": [],
"name": "UserQuotas",
"namespace": "http://docs.openstack.org/compute/ext/user_quotas/api/v1.1",
"updated": "2013-07-18T00:00:00+00:00"
},
{
"alias": "os-rescue",
"description": "Instance rescue mode.",
@ -584,6 +584,14 @@
"namespace": "http://docs.openstack.org/compute/ext/userdata/api/v1.1",
"updated": "2012-08-07T00:00:00+00:00"
},
{
"alias": "os-user-quotas",
"description": "Project user quota support.",
"links": [],
"name": "UserQuotas",
"namespace": "http://docs.openstack.org/compute/ext/user_quotas/api/v1.1",
"updated": "2013-07-18T00:00:00+00:00"
},
{
"alias": "os-virtual-interfaces",
"description": "Virtual interface support.",
@ -609,4 +617,4 @@
"updated": "2011-03-25T00:00:00+00:00"
}
]
}
}

View File

@ -52,6 +52,9 @@
<extension alias="os-aggregates" updated="2012-01-12T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/aggregates/api/v1.1" name="Aggregates">
<description>Admin-only aggregate administration.</description>
</extension>
<extension alias="os-assisted-volume-snapshots" updated="2013-08-15T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/assisted-volume-snapshots/api/v2" name="AssistedVolumeSnapshots">
<description>Assisted volume snapshots.</description>
</extension>
<extension alias="os-attach-interfaces" updated="2012-07-22T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/interfaces/api/v1.1" name="AttachInterfaces">
<description>Attach interface support.</description>
</extension>
@ -197,9 +200,6 @@
<extension alias="os-quota-sets" updated="2011-08-08T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" name="Quotas">
<description>Quotas management support.</description>
</extension>
<extension alias="os-user-quotas" updated="2013-07-18T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/user_quotas/api/v1.1" name="UserQuotas">
<description>Project user quota support.</description>
</extension>
<extension alias="os-rescue" updated="2011-08-18T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/rescue/api/v1.1" name="Rescue">
<description>Instance rescue mode.</description>
</extension>
@ -239,6 +239,9 @@
<extension alias="os-user-data" updated="2012-08-07T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/userdata/api/v1.1" name="UserData">
<description>Add user_data to the Create Server v1.1 API.</description>
</extension>
<extension alias="os-user-quotas" updated="2013-07-18T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/user_quotas/api/v1.1" name="UserQuotas">
<description>Project user quota support.</description>
</extension>
<extension alias="os-virtual-interfaces" updated="2011-08-17T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1" name="VirtualInterfaces">
<description>Virtual interface support.</description>
</extension>
@ -248,4 +251,4 @@
<extension alias="os-volumes" updated="2011-03-25T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
<description>Volumes support.</description>
</extension>
</extensions>
</extensions>

View File

@ -0,0 +1,10 @@
{
"snapshot": {
"display_name": "snap-001",
"display_description": "Daily backup",
"volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
"force": false,
"assisted": true,
"create_info": {}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<snapshot>
<display_name>snap-001</display_name>
<display_description>Daily backup</display_description>
<volume_id>521752a6-acf6-4b2d-bc7a-119f9148cd8c</volume_id>
<force>false</force>
<assisted>true</assisted>
<create_info/>
</snapshot>

View File

@ -0,0 +1,6 @@
{
"snapshot": {
"id": 100,
"volumeId": "521752a6-acf6-4b2d-bc7a-119f9148cd8c"
}
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<snapshot volumeId="521752a6-acf6-4b2d-bc7a-119f9148cd8c" id="100"/>

View File

@ -243,6 +243,8 @@
"compute_extension:migrations:index": "rule:admin_api",
"compute_extension:v3:os-migrations:index": "rule:admin_api",
"compute_extension:v3:os-migrations:discoverable": "",
"compute_extension:os-assisted-volume-snapshots:create": "rule:admin_api",
"compute_extension:os-assisted-volume-snapshots:delete": "rule:admin_api",
"volume:create": "",

View File

@ -0,0 +1,110 @@
# Copyright 2013 Red Hat, Inc.
#
# 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 nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import compute
from nova import exception
from nova.openstack.common.gettextutils import _
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
LOG = logging.getLogger(__name__)
authorize = extensions.extension_authorizer('compute',
'os-assisted-volume-snapshots')
def make_snapshot(elem):
elem.set('id')
elem.set('volumeId')
class SnapshotTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('snapshot', selector='snapshot')
make_snapshot(root)
return xmlutil.MasterTemplate(root, 1)
class AssistedVolumeSnapshotsController(wsgi.Controller):
def __init__(self):
self.compute_api = compute.API()
super(AssistedVolumeSnapshotsController, self).__init__()
@wsgi.serializers(xml=SnapshotTemplate)
def create(self, req, body):
"""Creates a new snapshot."""
context = req.environ['nova.context']
authorize(context, action='create')
if not self.is_valid_body(body, 'snapshot'):
raise webob.exc.HTTPBadRequest()
try:
snapshot = body['snapshot']
create_info = snapshot['create_info']
volume_id = snapshot['volume_id']
except KeyError:
raise webob.exc.HTTPBadRequest()
LOG.audit(_("Create assisted snapshot from volume %s"), volume_id,
context=context)
return self.compute_api.volume_snapshot_create(context, volume_id,
create_info)
def delete(self, req, id):
"""Delete a snapshot."""
context = req.environ['nova.context']
authorize(context, action='delete')
LOG.audit(_("Delete snapshot with id: %s"), id, context=context)
delete_metadata = {}
delete_metadata.update(req.GET)
try:
delete_info = jsonutils.loads(delete_metadata['delete_info'])
volume_id = delete_info['volume_id']
except (KeyError, ValueError) as e:
raise webob.exc.HTTPBadRequest(explanation=str(e))
try:
self.compute_api.volume_snapshot_delete(context, volume_id,
id, delete_info)
except exception.NotFound:
return webob.exc.HTTPNotFound()
return webob.Response(status_int=204)
class Assisted_volume_snapshots(extensions.ExtensionDescriptor):
"""Assisted volume snapshots."""
name = "AssistedVolumeSnapshots"
alias = "os-assisted-volume-snapshots"
namespace = ("http://docs.openstack.org/compute/ext/"
"assisted-volume-snapshots/api/v2")
updated = "2013-08-29T00:00:00-00:00"
def get_resources(self):
resource = extensions.ResourceExtension('os-assisted-volume-snapshots',
AssistedVolumeSnapshotsController())
return [resource]

View File

@ -1,4 +1,5 @@
# Copyright 2013 Josh Durgin
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -20,6 +21,8 @@ from oslo.config import cfg
import webob
from webob import exc
from nova.api.openstack.compute.contrib import assisted_volume_snapshots as \
assisted_snaps
from nova.api.openstack.compute.contrib import volumes
from nova.api.openstack import extensions
from nova.compute import api as compute_api
@ -79,6 +82,16 @@ def fake_delete_snapshot(self, context, snapshot_id):
pass
def fake_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id,
delete_info):
pass
def fake_compute_volume_snapshot_create(self, context, volume_id,
create_info):
pass
def fake_get_instance_bdms(self, context, instance):
return [{'id': 1,
'instance_uuid': instance['uuid'],
@ -793,3 +806,51 @@ class DeleteSnapshotTestCase(test.TestCase):
self.req.method = 'DELETE'
result = self.controller.delete(self.req, result['snapshot']['id'])
self.assertEqual(result.status_int, 202)
class AssistedSnapshotCreateTestCase(test.TestCase):
def setUp(self):
super(AssistedSnapshotCreateTestCase, self).setUp()
self.controller = assisted_snaps.AssistedVolumeSnapshotsController()
self.stubs.Set(compute_api.API, 'volume_snapshot_create',
fake_compute_volume_snapshot_create)
def test_assisted_create(self):
req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
body = {'snapshot': {'volume_id': 1, 'create_info': {}}}
req.method = 'POST'
self.controller.create(req, body=body)
def test_assisted_create_missing_create_info(self):
req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
body = {'snapshot': {'volume_id': 1}}
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
req, body=body)
class AssistedSnapshotDeleteTestCase(test.TestCase):
def setUp(self):
super(AssistedSnapshotDeleteTestCase, self).setUp()
self.controller = assisted_snaps.AssistedVolumeSnapshotsController()
self.stubs.Set(compute_api.API, 'volume_snapshot_delete',
fake_compute_volume_snapshot_delete)
def test_assisted_delete(self):
params = {
'delete_info': jsonutils.dumps({'volume_id': 1}),
}
req = fakes.HTTPRequest.blank(
'/v2/fake/os-assisted-volume-snapshots?%s' %
'&'.join(['%s=%s' % (k, v) for k, v in params.iteritems()]))
req.method = 'DELETE'
result = self.controller.delete(req, '5')
self.assertEqual(result.status_int, 204)
def test_assisted_delete_missing_delete_info(self):
req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
req.method = 'DELETE'
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
req, '5')

View File

@ -176,6 +176,7 @@ class ExtensionControllerTest(ExtensionTestCase):
self.ext_list = [
"AdminActions",
"Aggregates",
"AssistedVolumeSnapshots",
"AvailabilityZone",
"Agents",
"Certificates",

View File

@ -679,11 +679,20 @@ def stub_snapshot_create(self, context, volume_id, name, description):
display_description=description)
def stub_compute_volume_snapshot_create(self, context, volume_id, create_info):
return {'snapshot': {'id': 100, 'volumeId': volume_id}}
def stub_snapshot_delete(self, context, snapshot_id):
if snapshot_id == '-1':
raise exc.NotFound
def stub_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id,
delete_info):
pass
def stub_snapshot_get(self, context, snapshot_id):
if snapshot_id == '-1':
raise exc.NotFound

View File

@ -279,6 +279,8 @@ policy_data = """
"compute_extension:v3:os-used-limits:tenant": "is_admin:True",
"compute_extension:migrations:index": "is_admin:True",
"compute_extension:v3:os-migrations:index": "is_admin:True",
"compute_extension:os-assisted-volume-snapshots:create": "",
"compute_extension:os-assisted-volume-snapshots:delete": "",
"volume:create": "",
"volume:get": "",

View File

@ -136,6 +136,14 @@
"namespace": "http://docs.openstack.org/compute/ext/agents/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-assisted-volume-snapshots",
"description": "%(text)s",
"links": [],
"name": "AssistedVolumeSnapshots",
"namespace": "http://docs.openstack.org/compute/ext/assisted-volume-snapshots/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-attach-interfaces",
"description": "Attach interface support.",

View File

@ -228,4 +228,7 @@
<extension alias="os-migrations" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/migrations/api/v2.0" name="Migrations">
<description>%(text)s</description>
</extension>
<extension alias="os-assisted-volume-snapshots" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/assisted-volume-snapshots/api/v2" name="AssistedVolumeSnapshots">
<description>%(text)s</description>
</extension>
</extensions>

View File

@ -0,0 +1,10 @@
{
"snapshot": {
"display_name": "%(snapshot_name)s",
"display_description": "%(description)s",
"volume_id": "%(volume_id)s",
"force": false,
"assisted": true,
"create_info": {}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<snapshot>
<display_name>%(snapshot_name)s</display_name>
<display_description>%(description)s</display_description>
<volume_id>%(volume_id)s</volume_id>
<force>false</force>
<assisted>true</assisted>
<create_info/>
</snapshot>

View File

@ -0,0 +1,6 @@
{
"snapshot": {
"id": 100,
"volumeId": "%(uuid)s"
}
}

View File

@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<snapshot volumeId="521752a6-acf6-4b2d-bc7a-119f9148cd8c" id="100"/>

View File

@ -3697,6 +3697,47 @@ class SnapshotsSampleXmlTests(SnapshotsSampleJsonTests):
ctype = "xml"
class AssistedVolumeSnapshotsJsonTest(ApiSampleTestBaseV2):
"""Assisted volume snapshots."""
extension_name = ("nova.api.openstack.compute.contrib."
"assisted_volume_snapshots.Assisted_volume_snapshots")
def _create_assisted_snapshot(self, subs):
self.stubs.Set(compute_api.API, 'volume_snapshot_create',
fakes.stub_compute_volume_snapshot_create)
response = self._do_post("os-assisted-volume-snapshots",
"snapshot-create-assisted-req",
subs)
return response
def test_snapshots_create_assisted(self):
subs = {
'snapshot_name': 'snap-001',
'description': 'Daily backup',
'volume_id': '521752a6-acf6-4b2d-bc7a-119f9148cd8c'
}
subs.update(self._get_regexes())
response = self._create_assisted_snapshot(subs)
self._verify_response("snapshot-create-assisted-resp",
subs, response, 200)
def test_snapshots_delete_assisted(self):
self.stubs.Set(compute_api.API, 'volume_snapshot_delete',
fakes.stub_compute_volume_snapshot_delete)
snapshot_id = '100'
response = self._do_delete(
'os-assisted-volume-snapshots/%s?delete_info='
'{"volume_id":"521752a6-acf6-4b2d-bc7a-119f9148cd8c"}'
% snapshot_id)
self.assertEqual(response.status, 204)
self.assertEqual(response.read(), '')
class AssistedVolumeSnapshotsXmlTest(AssistedVolumeSnapshotsJsonTest):
ctype = "xml"
class VolumeAttachmentsSampleBase(ServersSampleBase):
def _stub_compute_api_get_instance_bdms(self, server_id):