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 d12d6bfc6cd6..5bb401e25e0e 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 5b9e20bb2a4a..4375c48d34c1 100644
--- a/nova/tests/integrated/test_api_samples.py
+++ b/nova/tests/integrated/test_api_samples.py
@@ -3700,6 +3700,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):