diff options
| author | Gaurav Gupta <gaurav@denali-systems.com> | 2011-10-29 17:20:08 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@gmail.com> | 2011-12-12 15:28:33 -0800 |
| commit | bfefe6317fde87f8ca7a4d28bed11d99f7029186 (patch) | |
| tree | ff2c44342880a617535ed1402c8ea8659ecf21ff | |
| parent | d3b75b75aa937380f04b5320b70c8673821af203 (diff) | |
| download | nova-bfefe6317fde87f8ca7a4d28bed11d99f7029186.tar.gz nova-bfefe6317fde87f8ca7a4d28bed11d99f7029186.tar.xz nova-bfefe6317fde87f8ca7a4d28bed11d99f7029186.zip | |
Added support for creating nova volume snapshots using OS API.
Fixes bug 883676
blueprint nova-volume-snapshot-backup-api
Change-Id: Id3e1ad39ef791b93dd014cada87c2d295454701f
| -rw-r--r-- | Authors | 1 | ||||
| -rw-r--r-- | nova/api/openstack/v2/contrib/volumes.py | 155 | ||||
| -rw-r--r-- | nova/tests/api/openstack/v2/contrib/test_snapshots.py | 299 | ||||
| -rw-r--r-- | nova/tests/api/openstack/v2/contrib/test_volumes.py | 6 | ||||
| -rw-r--r-- | nova/tests/api/openstack/v2/contrib/test_vsa.py | 4 |
5 files changed, 460 insertions, 5 deletions
@@ -51,6 +51,7 @@ Ewan Mellor <ewan.mellor@citrix.com> François Charlier <francois.charlier@enovance.com> Gabe Westmaas <gabe.westmaas@rackspace.com> Gary Kotton <garyk@radware.com> +Gaurav Gupta <gaurav@denali-systems.com> Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp> Hisaki Ohara <hisaki.ohara@intel.com> Ilya Alekseyev <ilyaalekseyev@acm.org> diff --git a/nova/api/openstack/v2/contrib/volumes.py b/nova/api/openstack/v2/contrib/volumes.py index 75d8af803..8a5e84c0d 100644 --- a/nova/api/openstack/v2/contrib/volumes.py +++ b/nova/api/openstack/v2/contrib/volumes.py @@ -24,11 +24,9 @@ from nova.api.openstack.v2 import servers from nova.api.openstack import wsgi from nova.api.openstack import xmlutil from nova import compute -from nova import db from nova import exception from nova import flags from nova import log as logging -from nova import quota from nova import volume from nova.volume import volume_types @@ -72,6 +70,7 @@ def _translate_volume_summary_view(context, vol): else: d['volumeType'] = vol['volume_type_id'] + d['snapshotId'] = vol['snapshot_id'] LOG.audit(_("vol=%s"), vol, context=context) if vol.get('volume_metadata'): @@ -153,7 +152,8 @@ class VolumeController(object): metadata = vol.get('metadata', None) - new_volume = self.volume_api.create(context, size, None, + new_volume = self.volume_api.create(context, size, + vol.get('snapshot_id'), vol.get('display_name'), vol.get('display_description'), volume_type=vol_type, @@ -176,6 +176,7 @@ def make_volume(elem): elem.set('displayName') elem.set('displayDescription') elem.set('volumeType') + elem.set('snapshotId') attachments = xmlutil.SubTemplateElement(elem, 'attachments') attachment = xmlutil.SubTemplateElement(attachments, 'attachment', @@ -394,6 +395,143 @@ class BootFromVolumeController(servers.Controller): return data.get('block_device_mapping') +def _translate_snapshot_detail_view(context, vol): + """Maps keys for snapshots details view.""" + + d = _translate_snapshot_summary_view(context, vol) + + # NOTE(gagupta): No additional data / lookups at the moment + return d + + +def _translate_snapshot_summary_view(context, vol): + """Maps keys for snapshots summary view.""" + d = {} + + d['id'] = vol['id'] + d['volumeId'] = vol['volume_id'] + d['status'] = vol['status'] + # NOTE(gagupta): We map volume_size as the snapshot size + d['size'] = vol['volume_size'] + d['createdAt'] = vol['created_at'] + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class SnapshotController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(SnapshotController, self).__init__() + + def show(self, req, id): + """Return data about the given snapshot.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get_snapshot(context, id) + except exception.NotFound: + return exc.HTTPNotFound() + + return {'snapshot': _translate_snapshot_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a snapshot.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete snapshot with id: %s"), id, context=context) + + try: + self.volume_api.delete_snapshot(context, snapshot_id=id) + except exception.NotFound: + return exc.HTTPNotFound() + return webob.Response(status_int=202) + + def index(self, req): + """Returns a summary list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_summary_view) + + def detail(self, req): + """Returns a detailed list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of snapshots, transformed through entity_maker.""" + context = req.environ['nova.context'] + + snapshots = self.volume_api.get_all_snapshots(context) + limited_list = common.limited(snapshots, req) + res = [entity_maker(context, snapshot) for snapshot in limited_list] + return {'snapshots': res} + + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + + if not body: + return exc.HTTPUnprocessableEntity() + + snapshot = body['snapshot'] + volume_id = snapshot['volume_id'] + force = snapshot.get('force', False) + LOG.audit(_("Create snapshot from volume %s"), volume_id, + context=context) + + if force: + new_snapshot = self.volume_api.create_snapshot_force(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + else: + new_snapshot = self.volume_api.create_snapshot(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + + retval = _translate_snapshot_detail_view(context, new_snapshot) + + return {'snapshot': retval} + + +def make_snapshot(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + 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 SnapshotsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshots') + elem = xmlutil.SubTemplateElement(root, 'snapshot', + selector='snapshots') + make_snapshot(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return SnapshotTemplate() + + def index(self): + return SnapshotsTemplate() + + def detail(self): + return SnapshotsTemplate() + + class Volumes(extensions.ExtensionDescriptor): """Volumes support""" @@ -449,4 +587,15 @@ class Volumes(extensions.ExtensionDescriptor): deserializer=deserializer) resources.append(res) + snapshot_serializers = { + 'application/xml': SnapshotSerializer(), + } + snap_serializer = wsgi.ResponseSerializer(snapshot_serializers) + + res = extensions.ResourceExtension('os-snapshots', + SnapshotController(), + serializer=snap_serializer, + collection_actions={'detail': 'GET'}) + resources.append(res) + return resources diff --git a/nova/tests/api/openstack/v2/contrib/test_snapshots.py b/nova/tests/api/openstack/v2/contrib/test_snapshots.py new file mode 100644 index 000000000..a04c9b734 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_snapshots.py @@ -0,0 +1,299 @@ +# Copyright 2011 Denali Systems, Inc. +# 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. + +import datetime +import json +import stubout + +from lxml import etree +import webob + +from nova.api.openstack.v2.contrib import volumes +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova import volume +from nova.tests.api.openstack import fakes + +FLAGS = flags.FLAGS + +LOG = logging.getLogger('nova.tests.api.openstack.snapshot') + +_last_param = {} + + +def _get_default_snapshot_param(): + return { + 'id': 123, + 'volume_id': 12, + 'status': 'available', + 'volume_size': 100, + 'created_at': None, + 'display_name': 'Default name', + 'display_description': 'Default description', + } + + +def stub_snapshot_create(self, context, volume_id, name, description): + global _last_param + snapshot = _get_default_snapshot_param() + snapshot['volume_id'] = volume_id + snapshot['display_name'] = name + snapshot['display_description'] = description + + LOG.debug(_("_create: %s"), snapshot) + _last_param = snapshot + return snapshot + + +def stub_snapshot_delete(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_delete: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + +def stub_snapshot_get(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_get: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + param = _get_default_snapshot_param() + param['id'] = snapshot_id + return param + + +def stub_snapshot_get_all(self, context): + LOG.debug(_("_get_all: %s"), locals()) + param = _get_default_snapshot_param() + param['id'] = 123 + return [param] + + +class SnapshotApiTest(test.TestCase): + def setUp(self): + super(SnapshotApiTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) + self.stubs.Set(volume.api.API, "create_snapshot_force", + stub_snapshot_create) + self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) + self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) + self.stubs.Set(volume.api.API, "get_all_snapshots", + stub_snapshot_get_all) + + self.context = context.get_admin_context() + + def tearDown(self): + self.stubs.UnsetAll() + super(SnapshotApiTest, self).tearDown() + + def test_snapshot_create(self): + global _last_param + _last_param = {} + + snapshot = {"volume_id": 12, + "force": False, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v1.1/777/os-snapshots') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_create_force(self): + global _last_param + _last_param = {} + + snapshot = {"volume_id": 12, + "force": True, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v1.1/777/os-snapshots') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create_force: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create_force: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_delete(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v1.1/777/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_delete_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v1.1/777/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_show(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v1.1/777/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + + LOG.debug(_("test_snapshot_show: resp=%s"), resp) + self.assertEqual(resp.status_int, 200) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + resp_dict = json.loads(resp.body) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id)) + + def test_snapshot_show_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v1.1/777/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_detail(self): + req = webob.Request.blank('/v1.1/777/os-snapshots/detail') + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_detail: resp_dict=%s"), resp_dict) + self.assertTrue('snapshots' in resp_dict) + resp_snapshots = resp_dict['snapshots'] + self.assertEqual(len(resp_snapshots), 1) + + resp_snapshot = resp_snapshots.pop() + self.assertEqual(resp_snapshot['id'], 123) + + +class SnapshotSerializerTest(test.TestCase): + def _verify_snapshot(self, snap, tree): + self.assertEqual(tree.tag, 'snapshot') + + for attr in ('id', 'status', 'size', 'createdAt', + 'displayName', 'displayDescription', 'volumeId'): + self.assertEqual(str(snap[attr]), tree.get(attr)) + + def test_snapshot_show_create_serializer(self): + serializer = volumes.SnapshotSerializer() + raw_snapshot = dict( + id='snap_id', + status='snap_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap_name', + displayDescription='snap_desc', + volumeId='vol_id', + ) + text = serializer.serialize(dict(snapshot=raw_snapshot), 'show') + + print text + tree = etree.fromstring(text) + + self._verify_snapshot(raw_snapshot, tree) + + def test_snapshot_index_detail_serializer(self): + serializer = volumes.SnapshotSerializer() + raw_snapshots = [dict( + id='snap1_id', + status='snap1_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap1_name', + displayDescription='snap1_desc', + volumeId='vol1_id', + ), + dict( + id='snap2_id', + status='snap2_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap2_name', + displayDescription='snap2_desc', + volumeId='vol2_id', + )] + text = serializer.serialize(dict(snapshots=raw_snapshots), 'index') + + print text + tree = etree.fromstring(text) + + self.assertEqual('snapshots', tree.tag) + self.assertEqual(len(raw_snapshots), len(tree)) + for idx, child in enumerate(tree): + self._verify_snapshot(raw_snapshots[idx], child) diff --git a/nova/tests/api/openstack/v2/contrib/test_volumes.py b/nova/tests/api/openstack/v2/contrib/test_volumes.py index f0e7a03b7..6dc4fb87c 100644 --- a/nova/tests/api/openstack/v2/contrib/test_volumes.py +++ b/nova/tests/api/openstack/v2/contrib/test_volumes.py @@ -100,7 +100,8 @@ class VolumeSerializerTest(test.TestCase): self.assertEqual(tree.tag, 'volume') for attr in ('id', 'status', 'size', 'availabilityZone', 'createdAt', - 'displayName', 'displayDescription', 'volumeType'): + 'displayName', 'displayDescription', 'volumeType', + 'snapshotId'): self.assertEqual(str(vol[attr]), tree.get(attr)) for child in tree: @@ -173,6 +174,7 @@ class VolumeSerializerTest(test.TestCase): displayName='vol_name', displayDescription='vol_desc', volumeType='vol_type', + snapshotId='snap_id', metadata=dict( foo='bar', baz='quux', @@ -201,6 +203,7 @@ class VolumeSerializerTest(test.TestCase): displayName='vol1_name', displayDescription='vol1_desc', volumeType='vol1_type', + snapshotId='snap1_id', metadata=dict( foo='vol1_foo', bar='vol1_bar', @@ -220,6 +223,7 @@ class VolumeSerializerTest(test.TestCase): displayName='vol2_name', displayDescription='vol2_desc', volumeType='vol2_type', + snapshotId='snap2_id', metadata=dict( foo='vol2_foo', bar='vol2_bar', diff --git a/nova/tests/api/openstack/v2/contrib/test_vsa.py b/nova/tests/api/openstack/v2/contrib/test_vsa.py index 03c9d4449..d48803bef 100644 --- a/nova/tests/api/openstack/v2/contrib/test_vsa.py +++ b/nova/tests/api/openstack/v2/contrib/test_vsa.py @@ -49,7 +49,7 @@ def _get_default_vsa_param(): 'image_name': None, 'availability_zone': None, 'storage': [], - 'shared': False + 'shared': False, } @@ -240,6 +240,7 @@ def _get_default_volume_param(): 'display_description': 'Default vol description', 'volume_type_id': 1, 'volume_metadata': [], + 'snapshot_id': None, } @@ -256,6 +257,7 @@ def stub_volume_create(self, context, size, snapshot_id, name, description, vol['size'] = size vol['display_name'] = name vol['display_description'] = description + vol['snapshot_id'] = snapshot_id return vol |
