From 0621c2507fbcdf96f3c230ce49f1bb103767f4e6 Mon Sep 17 00:00:00 2001 From: Eli Qiao Date: Wed, 15 Oct 2014 11:32:54 +0800 Subject: [PATCH] Port assisted-volume-snapshots extension to v2.1 This patch ports assisted-volume-snapshots to v2.1 and make v2 and v2.1 share unit test cases. This patch addes a schema to do the input validation for snapshots_create The differences between v2 and v3 are described on the wiki page https://wiki.openstack.org/wiki/NovaAPIv2tov3 . Partially implements blueprint v2-on-v3-api Change-Id: I5b7be1de8ac2628a287897dcc5ca0eaf7a8957a2 --- .../snapshot-create-assisted-req.json | 10 ++ .../snapshot-create-assisted-resp.json | 6 + etc/nova/policy.json | 3 + .../plugins/v3/assisted_volume_snapshots.py | 110 ++++++++++++++++++ .../schemas/v3/assisted_volume_snapshots.py | 50 ++++++++ .../openstack/compute/contrib/test_volumes.py | 52 +++++++-- nova/tests/unit/fake_policy.py | 2 + .../snapshot-create-assisted-req.json.tpl | 10 ++ .../snapshot-create-assisted-resp.json.tpl | 6 + .../v3/test_assisted_volume_snapshots.py | 52 +++++++++ setup.cfg | 1 + 11 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json create mode 100644 doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json create mode 100644 nova/api/openstack/compute/plugins/v3/assisted_volume_snapshots.py create mode 100644 nova/api/openstack/compute/schemas/v3/assisted_volume_snapshots.py create mode 100644 nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl create mode 100644 nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl create mode 100644 nova/tests/unit/integrated/v3/test_assisted_volume_snapshots.py diff --git a/doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json b/doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json new file mode 100644 index 000000000000..8c9a309ea92a --- /dev/null +++ b/doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json @@ -0,0 +1,10 @@ +{ + "snapshot": { + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "create_info": { + "snapshot_id": "421752a6-acf6-4b2d-bc7a-119f9148cd8c", + "type": "qcow2", + "new_file": "new_file_name" + } + } +} diff --git a/doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json b/doc/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json new file mode 100644 index 000000000000..acfc149658ce --- /dev/null +++ b/doc/v3/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/etc/nova/policy.json b/etc/nova/policy.json index 5d38b654a4b1..6d0a258eeedf 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -309,6 +309,9 @@ "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", + "compute_extension:v3:os-assisted-volume-snapshots:create": "rule:admin_api", + "compute_extension:v3:os-assisted-volume-snapshots:delete": "rule:admin_api", + "compute_extension:v3:os-assisted-volume-snapshots:discoverable": "", "compute_extension:console_auth_tokens": "rule:admin_api", "compute_extension:v3:os-console-auth-tokens": "rule:admin_api", "compute_extension:os-server-external-events:create": "rule:admin_api", diff --git a/nova/api/openstack/compute/plugins/v3/assisted_volume_snapshots.py b/nova/api/openstack/compute/plugins/v3/assisted_volume_snapshots.py new file mode 100644 index 000000000000..a19dd940dcb5 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/assisted_volume_snapshots.py @@ -0,0 +1,110 @@ +# Copyright 2013 Red Hat, Inc. +# Copyright 2014 IBM Corp. +# 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. + +"""The Assisted volume snapshots extension.""" + +from oslo.serialization import jsonutils +import six +from webob import exc + +from nova.api.openstack.compute.schemas.v3 import assisted_volume_snapshots +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api import validation +from nova import compute +from nova import exception +from nova.i18n import _ +from nova.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) +ALIAS = 'os-assisted-volume-snapshots' +authorize = extensions.extension_authorizer('compute', + 'v3:' + ALIAS) + + +class AssistedVolumeSnapshotsController(wsgi.Controller): + """The Assisted volume snapshots API controller for the OpenStack API.""" + + def __init__(self): + self.compute_api = compute.API() + super(AssistedVolumeSnapshotsController, self).__init__() + + @extensions.expected_errors(400) + @validation.schema(assisted_volume_snapshots.snapshots_create) + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + authorize(context, action='create') + + snapshot = body['snapshot'] + create_info = snapshot['create_info'] + volume_id = snapshot['volume_id'] + + LOG.audit(_("Create assisted snapshot from volume %s"), volume_id, + context=context) + try: + return self.compute_api.volume_snapshot_create(context, volume_id, + create_info) + except (exception.VolumeBDMNotFound, + exception.InvalidVolume) as error: + raise exc.HTTPBadRequest(explanation=error.format_message()) + + @wsgi.response(204) + @extensions.expected_errors((400, 404)) + 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 exc.HTTPBadRequest(explanation=six.text_type(e)) + + try: + self.compute_api.volume_snapshot_delete(context, volume_id, + id, delete_info) + except (exception.VolumeBDMNotFound, + exception.InvalidVolume) as error: + raise exc.HTTPBadRequest(explanation=error.format_message()) + except exception.NotFound as e: + return exc.HTTPNotFound(explanation=e.format_message()) + + +class AssistedVolumeSnapshots(extensions.V3APIExtensionBase): + """Assisted volume snapshots.""" + + name = "AssistedVolumeSnapshots" + alias = ALIAS + version = 1 + + def get_resources(self): + res = [extensions.ResourceExtension(ALIAS, + AssistedVolumeSnapshotsController())] + return res + + def get_controller_extensions(self): + """It's an abstract function V3APIExtensionBase and the extension + will not be loaded without it. + """ + return [] diff --git a/nova/api/openstack/compute/schemas/v3/assisted_volume_snapshots.py b/nova/api/openstack/compute/schemas/v3/assisted_volume_snapshots.py new file mode 100644 index 000000000000..4cb2b71bd66e --- /dev/null +++ b/nova/api/openstack/compute/schemas/v3/assisted_volume_snapshots.py @@ -0,0 +1,50 @@ +# Copyright 2014 IBM Corporation. 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. + +snapshots_create = { + 'type': 'object', + 'properties': { + 'snapshot': { + 'type': 'object', + 'properties': { + 'volume_id': { + 'type': 'string', 'minLength': 1, + }, + 'create_info': { + 'type': 'object', + 'properties': { + 'snapshot_id': { + 'type': 'string', 'minLength': 1, + }, + 'type': { + 'type': 'string', 'enum': ['qcow2'], + }, + 'new_file': { + 'type': 'string', 'minLength': 1, + }, + 'id': { + 'type': 'string', 'minLength': 1, + }, + }, + 'required': ['snapshot_id', 'type', 'new_file'], + 'additionalProperties': False, + }, + }, + 'required': ['volume_id', 'create_info'], + 'additionalProperties': False, + }, + 'required': ['snapshot'], + 'additionalProperties': False, + }, +} diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py index 8a44a1892b38..88f89dee5757 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py @@ -25,8 +25,10 @@ import webob from webob import exc from nova.api.openstack.compute.contrib import assisted_volume_snapshots as \ - assisted_snaps + assisted_snaps_v2 from nova.api.openstack.compute.contrib import volumes +from nova.api.openstack.compute.plugins.v3 import assisted_volume_snapshots as \ + assisted_snaps_v21 from nova.api.openstack.compute.plugins.v3 import volumes as volumes_v3 from nova.api.openstack import extensions from nova.compute import api as compute_api @@ -1042,17 +1044,25 @@ class DeleteSnapshotTestCaseV2(DeleteSnapshotTestCaseV21): snapshot_cls = volumes.SnapshotController -class AssistedSnapshotCreateTestCase(test.TestCase): - def setUp(self): - super(AssistedSnapshotCreateTestCase, self).setUp() +class AssistedSnapshotCreateTestCaseV21(test.TestCase): + assisted_snaps = assisted_snaps_v21 + bad_request = exception.ValidationError - self.controller = assisted_snaps.AssistedVolumeSnapshotsController() + def setUp(self): + super(AssistedSnapshotCreateTestCaseV21, self).setUp() + + self.controller = \ + self.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': {}}} + body = {'snapshot': + {'volume_id': '1', + 'create_info': {'type': 'qcow2', + 'new_file': 'new_file', + 'snapshot_id': 'snapshot_id'}}} req.method = 'POST' self.controller.create(req, body=body) @@ -1060,15 +1070,26 @@ class AssistedSnapshotCreateTestCase(test.TestCase): 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, + self.assertRaises(self.bad_request, self.controller.create, req, body=body) -class AssistedSnapshotDeleteTestCase(test.TestCase): - def setUp(self): - super(AssistedSnapshotDeleteTestCase, self).setUp() +class AssistedSnapshotCreateTestCaseV2(AssistedSnapshotCreateTestCaseV21): + assisted_snaps = assisted_snaps_v2 + bad_request = webob.exc.HTTPBadRequest - self.controller = assisted_snaps.AssistedVolumeSnapshotsController() + +class AssistedSnapshotDeleteTestCaseV21(test.TestCase): + assisted_snaps = assisted_snaps_v21 + + def _check_status(self, expected_status, res, controller_method): + self.assertEqual(expected_status, controller_method.wsgi_code) + + def setUp(self): + super(AssistedSnapshotDeleteTestCaseV21, self).setUp() + + self.controller = \ + self.assisted_snaps.AssistedVolumeSnapshotsController() self.stubs.Set(compute_api.API, 'volume_snapshot_delete', fake_compute_volume_snapshot_delete) @@ -1081,10 +1102,17 @@ class AssistedSnapshotDeleteTestCase(test.TestCase): '&'.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) + self._check_status(204, result, self.controller.delete) 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') + + +class AssistedSnapshotDeleteTestCaseV2(AssistedSnapshotDeleteTestCaseV21): + assisted_snaps = assisted_snaps_v2 + + def _check_status(self, expected_status, res, controller_method): + self.assertEqual(expected_status, res.status_int) diff --git a/nova/tests/unit/fake_policy.py b/nova/tests/unit/fake_policy.py index 1c1d5156f00b..e99919f0c58d 100644 --- a/nova/tests/unit/fake_policy.py +++ b/nova/tests/unit/fake_policy.py @@ -327,6 +327,8 @@ policy_data = """ "compute_extension:v3:os-migrations:index": "is_admin:True", "compute_extension:os-assisted-volume-snapshots:create": "", "compute_extension:os-assisted-volume-snapshots:delete": "", + "compute_extension:v3:os-assisted-volume-snapshots:create": "", + "compute_extension:v3:os-assisted-volume-snapshots:delete": "", "compute_extension:console_auth_tokens": "is_admin:True", "compute_extension:v3:os-console-auth-tokens": "is_admin:True", "compute_extension:os-server-external-events:create": "rule:admin_api", diff --git a/nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl b/nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl new file mode 100644 index 000000000000..ff152e1db864 --- /dev/null +++ b/nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-req.json.tpl @@ -0,0 +1,10 @@ +{ + "snapshot": { + "volume_id": "%(volume_id)s", + "create_info": { + "snapshot_id": "%(snapshot_id)s", + "type": "%(type)s", + "new_file": "%(new_file)s" + } + } +} diff --git a/nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl b/nova/tests/unit/integrated/v3/api_samples/os-assisted-volume-snapshots/snapshot-create-assisted-resp.json.tpl new file mode 100644 index 000000000000..8d4e7f570982 --- /dev/null +++ b/nova/tests/unit/integrated/v3/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/unit/integrated/v3/test_assisted_volume_snapshots.py b/nova/tests/unit/integrated/v3/test_assisted_volume_snapshots.py new file mode 100644 index 000000000000..1c107f3d9662 --- /dev/null +++ b/nova/tests/unit/integrated/v3/test_assisted_volume_snapshots.py @@ -0,0 +1,52 @@ +# Copyright 2014 IBM Corp. +# +# 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 nova.compute import api as compute_api +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.integrated.v3 import test_servers + + +class AssistedVolumeSnapshotsJsonTests(test_servers.ServersSampleBase): + extension_name = "os-assisted-volume-snapshots" + + def test_create(self): + """Create a volume snapshots.""" + self.stubs.Set(compute_api.API, 'volume_snapshot_create', + fakes.stub_compute_volume_snapshot_create) + + subs = { + 'volume_id': '521752a6-acf6-4b2d-bc7a-119f9148cd8c', + 'snapshot_id': '421752a6-acf6-4b2d-bc7a-119f9148cd8c', + 'type': 'qcow2', + 'new_file': 'new_file_name' + } + + response = self._do_post("os-assisted-volume-snapshots", + "snapshot-create-assisted-req", + subs) + subs.update(self._get_regexes()) + 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_code, 204) + self.assertEqual(response.content, '') diff --git a/setup.cfg b/setup.cfg index c883ec6bb3c0..bd52601f3540 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ nova.api.v3.extensions = admin_password = nova.api.openstack.compute.plugins.v3.admin_password:AdminPassword agents = nova.api.openstack.compute.plugins.v3.agents:Agents aggregates = nova.api.openstack.compute.plugins.v3.aggregates:Aggregates + assisted_volume_snapshots = nova.api.openstack.compute.plugins.v3.assisted_volume_snapshots:AssistedVolumeSnapshots attach_interfaces = nova.api.openstack.compute.plugins.v3.attach_interfaces:AttachInterfaces availability_zone = nova.api.openstack.compute.plugins.v3.availability_zone:AvailabilityZone baremetal_nodes = nova.api.openstack.compute.plugins.v3.baremetal_nodes:BareMetalNodes