From fa13644b058e47fda6bdd28a853cc46597c80a97 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 16 Aug 2013 20:44:37 +0000 Subject: [PATCH] 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 --- .../all_extensions/extensions-get-resp.json | 26 +++-- .../all_extensions/extensions-get-resp.xml | 11 +- .../snapshot-create-assisted-req.json | 10 ++ .../snapshot-create-assisted-req.xml | 9 ++ .../snapshot-create-assisted-resp.json | 6 + .../snapshot-create-assisted-resp.xml | 2 + etc/nova/policy.json | 2 + .../contrib/assisted_volume_snapshots.py | 110 ++++++++++++++++++ .../openstack/compute/contrib/test_volumes.py | 61 ++++++++++ .../api/openstack/compute/test_extensions.py | 1 + nova/tests/api/openstack/fakes.py | 9 ++ nova/tests/fake_policy.py | 2 + .../extensions-get-resp.json.tpl | 8 ++ .../extensions-get-resp.xml.tpl | 3 + .../snapshot-create-assisted-req.json.tpl | 10 ++ .../snapshot-create-assisted-req.xml.tpl | 9 ++ .../snapshot-create-assisted-resp.json.tpl | 6 + .../snapshot-create-assisted-resp.xml.tpl | 2 + nova/tests/integrated/test_api_samples.py | 41 +++++++ 19 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json create mode 100644 doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml create mode 100644 doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json create mode 100644 doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml create mode 100644 nova/api/openstack/compute/contrib/assisted_volume_snapshots.py create mode 100644 nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index e340dda72669..4db707bc1d86 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -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" } ] -} +} \ No newline at end of file diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 73ea795a7247..61b43a0fda44 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -52,6 +52,9 @@ Admin-only aggregate administration. + + Assisted volume snapshots. + Attach interface support. @@ -197,9 +200,6 @@ Quotas management support. - - Project user quota support. - Instance rescue mode. @@ -239,6 +239,9 @@ Add user_data to the Create Server v1.1 API. + + Project user quota support. + Virtual interface support. @@ -248,4 +251,4 @@ Volumes support. - + \ No newline at end of file diff --git a/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json new file mode 100644 index 000000000000..4425b9dc7ee4 --- /dev/null +++ b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json @@ -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": {} + } +} diff --git a/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml new file mode 100644 index 000000000000..24100bcc670a --- /dev/null +++ b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml @@ -0,0 +1,9 @@ + + + snap-001 + Daily backup + 521752a6-acf6-4b2d-bc7a-119f9148cd8c + false + true + + \ No newline at end of file diff --git a/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json new file mode 100644 index 000000000000..acfc149658ce --- /dev/null +++ b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json @@ -0,0 +1,6 @@ +{ + "snapshot": { + "id": 100, + "volumeId": "521752a6-acf6-4b2d-bc7a-119f9148cd8c" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml new file mode 100644 index 000000000000..419d6d40669c --- /dev/null +++ b/doc/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 55bf5b2fc40b..8bbd2545ac5a 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -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": "", diff --git a/nova/api/openstack/compute/contrib/assisted_volume_snapshots.py b/nova/api/openstack/compute/contrib/assisted_volume_snapshots.py new file mode 100644 index 000000000000..94dd50b60533 --- /dev/null +++ b/nova/api/openstack/compute/contrib/assisted_volume_snapshots.py @@ -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] diff --git a/nova/tests/api/openstack/compute/contrib/test_volumes.py b/nova/tests/api/openstack/compute/contrib/test_volumes.py index 3e11367481e8..a5da6e0db5a2 100644 --- a/nova/tests/api/openstack/compute/contrib/test_volumes.py +++ b/nova/tests/api/openstack/compute/contrib/test_volumes.py @@ -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') diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index 2bedd69272da..726737a8bc92 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -176,6 +176,7 @@ class ExtensionControllerTest(ExtensionTestCase): self.ext_list = [ "AdminActions", "Aggregates", + "AssistedVolumeSnapshots", "AvailabilityZone", "Agents", "Certificates", diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 8a1e440a7ecc..df5fe8a6f38b 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -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 diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index afada9433c13..a50fd2e8920b 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -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": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index e00730a53b22..b4607875cf56 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -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.", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 81955c4b78f6..a2840f77abe6 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -228,4 +228,7 @@ %(text)s + + %(text)s + diff --git a/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl new file mode 100644 index 000000000000..a1b94d1c7294 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl @@ -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": {} + } +} diff --git a/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml.tpl b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml.tpl new file mode 100644 index 000000000000..45feb5077f92 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.xml.tpl @@ -0,0 +1,9 @@ + + + %(snapshot_name)s + %(description)s + %(volume_id)s + false + true + + diff --git a/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl new file mode 100644 index 000000000000..8d4e7f570982 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl @@ -0,0 +1,6 @@ +{ + "snapshot": { + "id": 100, + "volumeId": "%(uuid)s" + } +} diff --git a/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml.tpl b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml.tpl new file mode 100644 index 000000000000..5da7d148b153 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.xml.tpl @@ -0,0 +1,2 @@ + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index ff79462b558d..76ec458bc171 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -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):