summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Waldon <brian.waldon@rackspace.com>2011-03-25 10:56:53 -0400
committerBrian Waldon <brian.waldon@rackspace.com>2011-03-25 10:56:53 -0400
commit596e0b37f1e72330a99bcfbf0805bc70e8b49991 (patch)
treebccdf6d8104c03e01707564ade289480b232c482
parent6c29d4a09574fd230a5fe3b0bbfa615fe18b328c (diff)
parent48c9b4e14ad1b03e9cf3db068123c04ce1db01ce (diff)
downloadnova-596e0b37f1e72330a99bcfbf0805bc70e8b49991.tar.gz
nova-596e0b37f1e72330a99bcfbf0805bc70e8b49991.tar.xz
nova-596e0b37f1e72330a99bcfbf0805bc70e8b49991.zip
merging trunk
-rw-r--r--nova/api/openstack/__init__.py7
-rw-r--r--nova/api/openstack/image_metadata.py93
-rw-r--r--nova/tests/api/openstack/fakes.py8
-rw-r--r--nova/tests/api/openstack/test_image_metadata.py166
4 files changed, 272 insertions, 2 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 727655a86..e8c49cf7b 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -33,6 +33,7 @@ from nova.api.openstack import backup_schedules
from nova.api.openstack import consoles
from nova.api.openstack import flavors
from nova.api.openstack import images
+from nova.api.openstack import image_metadata
from nova.api.openstack import limits
from nova.api.openstack import servers
from nova.api.openstack import server_metadata
@@ -150,6 +151,12 @@ class APIRouterV11(APIRouter):
controller=servers.ControllerV11(),
collection={'detail': 'GET'},
member=self.server_members)
+
+ mapper.resource("image_meta", "meta",
+ controller=image_metadata.Controller(),
+ parent_resource=dict(member_name='image',
+ collection_name='images'))
+
mapper.resource("server_meta", "meta",
controller=server_metadata.Controller(),
parent_resource=dict(member_name='server',
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
new file mode 100644
index 000000000..c9d6ac532
--- /dev/null
+++ b/nova/api/openstack/image_metadata.py
@@ -0,0 +1,93 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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.
+
+from webob import exc
+
+from nova import flags
+from nova import utils
+from nova import wsgi
+from nova.api.openstack import faults
+
+
+FLAGS = flags.FLAGS
+
+
+class Controller(wsgi.Controller):
+ """The image metadata API controller for the Openstack API"""
+
+ def __init__(self):
+ self.image_service = utils.import_object(FLAGS.image_service)
+ super(Controller, self).__init__()
+
+ def _get_metadata(self, context, image_id, image=None):
+ if not image:
+ image = self.image_service.show(context, image_id)
+ metadata = image.get('properties', {})
+ return metadata
+
+ def index(self, req, image_id):
+ """Returns the list of metadata for a given instance"""
+ context = req.environ['nova.context']
+ metadata = self._get_metadata(context, image_id)
+ return dict(metadata=metadata)
+
+ def show(self, req, image_id, id):
+ context = req.environ['nova.context']
+ metadata = self._get_metadata(context, image_id)
+ if id in metadata:
+ return {id: metadata[id]}
+ else:
+ return faults.Fault(exc.HTTPNotFound())
+
+ def create(self, req, image_id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id, img)
+ if 'metadata' in body:
+ for key, value in body['metadata'].iteritems():
+ metadata[key] = value
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
+ return dict(metadata=metadata)
+
+ def update(self, req, image_id, id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ if not id in body:
+ expl = _('Request body and URI mismatch')
+ raise exc.HTTPBadRequest(explanation=expl)
+ if len(body) > 1:
+ expl = _('Request body contains too many items')
+ raise exc.HTTPBadRequest(explanation=expl)
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id, img)
+ metadata[id] = body[id]
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
+
+ return req.body
+
+ def delete(self, req, image_id, id):
+ context = req.environ['nova.context']
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id)
+ if not id in metadata:
+ return faults.Fault(exc.HTTPNotFound())
+ metadata.pop(id)
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 3cc68a536..91819d5f5 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -184,15 +184,19 @@ def stub_out_glance(stubs, initial_fixtures=None):
for _ in range(20))
image_meta['id'] = image_id
self.fixtures.append(image_meta)
- return image_meta
+ return copy.deepcopy(image_meta)
def fake_update_image(self, image_id, image_meta, data=None):
+ for attr in ('created_at', 'updated_at', 'deleted_at', 'deleted'):
+ if attr in image_meta:
+ del image_meta[attr]
+
f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
f.update(image_meta)
- return f
+ return copy.deepcopy(f)
def fake_delete_image(self, image_id):
f = self._find_image(image_id)
diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py
new file mode 100644
index 000000000..9be753f84
--- /dev/null
+++ b/nova/tests/api/openstack/test_image_metadata.py
@@ -0,0 +1,166 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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 json
+import stubout
+import unittest
+import webob
+
+
+from nova import flags
+from nova.api import openstack
+from nova.tests.api.openstack import fakes
+import nova.wsgi
+
+
+FLAGS = flags.FLAGS
+
+
+class ImageMetaDataTest(unittest.TestCase):
+
+ IMAGE_FIXTURES = [
+ {'status': 'active',
+ 'name': 'image1',
+ 'deleted': False,
+ 'container_format': None,
+ 'created_at': '2011-03-22T17:40:15',
+ 'disk_format': None,
+ 'updated_at': '2011-03-22T17:40:15',
+ 'id': '1',
+ 'location': 'file:///var/lib/glance/images/1',
+ 'is_public': True,
+ 'deleted_at': None,
+ 'properties': {
+ 'type': 'ramdisk',
+ 'key1': 'value1',
+ 'key2': 'value2'
+ },
+ 'size': 5882349},
+ {'status': 'active',
+ 'name': 'image2',
+ 'deleted': False,
+ 'container_format': None,
+ 'created_at': '2011-03-22T17:40:15',
+ 'disk_format': None,
+ 'updated_at': '2011-03-22T17:40:15',
+ 'id': '2',
+ 'location': 'file:///var/lib/glance/images/2',
+ 'is_public': True,
+ 'deleted_at': None,
+ 'properties': {
+ 'type': 'ramdisk',
+ 'key1': 'value1',
+ 'key2': 'value2'
+ },
+ 'size': 5882349},
+ ]
+
+ def setUp(self):
+ super(ImageMetaDataTest, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ self.orig_image_service = FLAGS.image_service
+ FLAGS.image_service = 'nova.image.glance.GlanceImageService'
+ fakes.FakeAuthManager.auth_data = {}
+ fakes.FakeAuthDatabase.data = {}
+ fakes.stub_out_auth(self.stubs)
+ fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES)
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+ FLAGS.image_service = self.orig_image_service
+ super(ImageMetaDataTest, self).tearDown()
+
+ def test_index(self):
+ req = webob.Request.blank('/v1.1/images/1/meta')
+ req.environ['api.version'] = '1.1'
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual('value1', res_dict['metadata']['key1'])
+
+ def test_show(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/key1')
+ req.environ['api.version'] = '1.1'
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual('value1', res_dict['key1'])
+
+ def test_show_not_found(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/key9')
+ req.environ['api.version'] = '1.1'
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(404, res.status_int)
+
+ def test_create(self):
+ req = webob.Request.blank('/v1.1/images/2/meta')
+ req.environ['api.version'] = '1.1'
+ req.method = 'POST'
+ req.body = '{"metadata": {"key9": "value9"}}'
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual('value9', res_dict['metadata']['key9'])
+ # other items should not be modified
+ self.assertEqual('value1', res_dict['metadata']['key1'])
+ self.assertEqual('value2', res_dict['metadata']['key2'])
+ self.assertEqual(1, len(res_dict))
+
+ def test_update_item(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/key1')
+ req.environ['api.version'] = '1.1'
+ req.method = 'PUT'
+ req.body = '{"key1": "zz"}'
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ res_dict = json.loads(res.body)
+ self.assertEqual('zz', res_dict['key1'])
+
+ def test_update_item_too_many_keys(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/key1')
+ req.environ['api.version'] = '1.1'
+ req.method = 'PUT'
+ req.body = '{"key1": "value1", "key2": "value2"}'
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_update_item_body_uri_mismatch(self):
+ req = webob.Request.blank('/v1.1/images/1/meta/bad')
+ req.environ['api.version'] = '1.1'
+ req.method = 'PUT'
+ req.body = '{"key1": "value1"}'
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(400, res.status_int)
+
+ def test_delete(self):
+ req = webob.Request.blank('/v1.1/images/2/meta/key1')
+ req.environ['api.version'] = '1.1'
+ req.method = 'DELETE'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+
+ def test_delete_not_found(self):
+ req = webob.Request.blank('/v1.1/images/2/meta/blah')
+ req.environ['api.version'] = '1.1'
+ req.method = 'DELETE'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)