From: Nikola Dipanov Date: Fri, 7 Dec 2012 14:53:28 +0000 (+0100) Subject: Add image metadata API extension X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=349d5bdf0ed6fb18308c1e1e352d726ed9e00d8f;p=openstack-build%2Fcinder-build.git Add image metadata API extension This patch introduces an API extension that will expose the volume metadata inherited from the image if the volume was created from an image. This data will be used by Nova when booting from volume without any image supplied. In addition, the patch introduces a helper method on the volume.API class for accessing the image metadata and also updates the policy.json files to be aware of the new extension. This patch is part of the blueprint improve-boot-from-volume This patch also introduces DocImpact as it extends the cinder API Change-Id: I64515cc02f8de863e65e96c66eb1fda1a1954ac7 --- diff --git a/cinder/api/contrib/volume_image_metadata.py b/cinder/api/contrib/volume_image_metadata.py new file mode 100644 index 000000000..58e5cd295 --- /dev/null +++ b/cinder/api/contrib/volume_image_metadata.py @@ -0,0 +1,106 @@ +# Copyright 2012 OpenStack, LLC. +# +# 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 Volume Image Metadata API extension.""" + +from cinder.api import extensions +from cinder.api.openstack import wsgi +from cinder.api import xmlutil +from cinder import volume + + +authorize = extensions.soft_extension_authorizer('volume', + 'volume_image_metadata') + + +class VolumeImageMetadataController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(VolumeImageMetadataController, self).__init__(*args, **kwargs) + self.volume_api = volume.API() + + def _add_image_metadata(self, context, resp_volume): + try: + image_meta = self.volume_api.get_volume_image_metadata( + context, resp_volume) + except Exception: + return + else: + if image_meta: + resp_volume['volume_image_metadata'] = dict( + image_meta.iteritems()) + + @wsgi.extends + def show(self, req, resp_obj, id): + context = req.environ['cinder.context'] + if authorize(context): + resp_obj.attach(xml=VolumeImageMetadataTemplate()) + self._add_image_metadata(context, resp_obj.obj['volume']) + + @wsgi.extends + def detail(self, req, resp_obj): + context = req.environ['cinder.context'] + if authorize(context): + resp_obj.attach(xml=VolumesImageMetadataTemplate()) + for volume in list(resp_obj.obj.get('volumes', [])): + self._add_image_metadata(context, volume) + + +class Volume_image_metadata(extensions.ExtensionDescriptor): + """Show image metadata associated with the volume""" + + name = "VolumeImageMetadata" + alias = "os-vol-image-meta" + namespace = ("http://docs.openstack.org/volume/ext/" + "volume_image_metadata/api/v1") + updated = "2012-12-07T00:00:00+00:00" + + def get_controller_extensions(self): + controller = VolumeImageMetadataController() + extension = extensions.ControllerExtension(self, 'volumes', controller) + return [extension] + + +class VolumeImageMetadataMetadataTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_image_metadata', + selector='volume_image_metadata') + elem = xmlutil.SubTemplateElement(root, 'meta', + selector=xmlutil.get_items) + elem.set('key', 0) + elem.text = 1 + + return xmlutil.MasterTemplate(root, 1) + + +class VolumeImageMetadataTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + root.append(VolumeImageMetadataMetadataTemplate()) + + alias = Volume_image_metadata.alias + namespace = Volume_image_metadata.namespace + + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) + + +class VolumesImageMetadataTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volume') + elem.append(VolumeImageMetadataMetadataTemplate()) + + alias = Volume_image_metadata.alias + namespace = Volume_image_metadata.namespace + + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) diff --git a/cinder/tests/api/contrib/test_volume_image_metadata.py b/cinder/tests/api/contrib/test_volume_image_metadata.py new file mode 100644 index 000000000..2c343cfcb --- /dev/null +++ b/cinder/tests/api/contrib/test_volume_image_metadata.py @@ -0,0 +1,130 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# +# 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 uuid +from xml.dom import minidom + +import webob + +from cinder.api import common +from cinder.api.openstack.wsgi import MetadataXMLDeserializer +from cinder.api.openstack.wsgi import XMLDeserializer +from cinder import test +from cinder.tests.api import fakes +from cinder import volume + + +def fake_volume_get(*args, **kwargs): + return { + 'id': 'fake', + 'host': 'host001', + 'status': 'available', + 'size': 5, + 'availability_zone': 'somewhere', + 'created_at': datetime.datetime.now(), + 'attach_status': None, + 'display_name': 'anothervolume', + 'display_description': 'Just another volume!', + 'volume_type_id': None, + 'snapshot_id': None, + 'project_id': 'fake', + } + + +def fake_volume_get_all(*args, **kwargs): + return [fake_volume_get()] + + +fake_image_metadata = { + 'image_id': 'someid', + 'image_name': 'fake', + 'kernel_id': 'somekernel', + 'ramdisk_id': 'someramdisk', +} + + +def fake_get_volume_image_metadata(*args, **kwargs): + return fake_image_metadata + + +class VolumeImageMetadataTest(test.TestCase): + content_type = 'application/json' + + def setUp(self): + super(VolumeImageMetadataTest, self).setUp() + self.stubs.Set(volume.API, 'get', fake_volume_get) + self.stubs.Set(volume.API, 'get_all', fake_volume_get_all) + self.stubs.Set(volume.API, 'get_volume_image_metadata', + fake_get_volume_image_metadata) + self.UUID = uuid.uuid4() + + def _make_request(self, url): + req = webob.Request.blank(url) + req.accept = self.content_type + res = req.get_response(fakes.wsgi_app()) + return res + + def _get_image_metadata(self, body): + return json.loads(body)['volume']['volume_image_metadata'] + + def _get_image_metadata_list(self, body): + return [ + volume['volume_image_metadata'] + for volume in json.loads(body)['volumes'] + ] + + def test_get_volume(self): + res = self._make_request('/v2/fake/volumes/%s' % self.UUID) + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_image_metadata(res.body), + fake_image_metadata) + + def test_list_detail_volumes(self): + res = self._make_request('/v2/fake/volumes/detail') + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_image_metadata_list(res.body)[0], + fake_image_metadata) + + +class ImageMetadataXMLDeserializer(common.MetadataXMLDeserializer): + metadata_node_name = "volume_image_metadata" + + +class VolumeImageMetadataXMLTest(VolumeImageMetadataTest): + content_type = 'application/xml' + + def _get_image_metadata(self, body): + deserializer = XMLDeserializer() + volume = deserializer.find_first_child_named( + minidom.parseString(body), 'volume') + image_metadata = deserializer.find_first_child_named( + volume, 'volume_image_metadata') + return MetadataXMLDeserializer().extract_metadata(image_metadata) + + def _get_image_metadata_list(self, body): + deserializer = XMLDeserializer() + volumes = deserializer.find_first_child_named( + minidom.parseString(body), 'volumes') + volume_list = deserializer.find_children_named(volumes, 'volume') + image_metadata_list = [ + deserializer.find_first_child_named( + volume, 'volume_image_metadata' + ) + for volume in volume_list] + return map(MetadataXMLDeserializer().extract_metadata, + image_metadata_list) diff --git a/cinder/tests/policy.json b/cinder/tests/policy.json index 3576076b8..a2c226f4b 100644 --- a/cinder/tests/policy.json +++ b/cinder/tests/policy.json @@ -35,6 +35,7 @@ "volume_extension:types_manage": [], "volume_extension:types_extra_specs": [], "volume_extension:extended_snapshot_attributes": [], + "volume_extension:volume_image_metadata": [], "volume_extension:volume_host_attribute": [["rule:admin_api"]], "volume_extension:volume_tenant_attribute": [["rule:admin_api"]], "volume_extension:hosts": [["rule:admin_api"]] diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 5a99c384a..5c9523368 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -489,6 +489,13 @@ class API(base.Base): return i['value'] return None + @wrap_check_policy + def get_volume_image_metadata(self, context, volume): + db_data = self.db.volume_glance_metadata_get(context, volume['id']) + return dict( + (meta_entry.key, meta_entry.value) for meta_entry in db_data + ) + def _check_volume_availability(self, context, volume, force): """Check if the volume can be used.""" if volume['status'] not in ['available', 'in-use']: diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 9b088780a..7e917b7fa 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -14,6 +14,7 @@ "volume_extension:types_manage": [["rule:admin_api"]], "volume_extension:types_extra_specs": [["rule:admin_api"]], "volume_extension:extended_snapshot_attributes": [], + "volume_extension:volume_image_metadata": [], "volume_extension:quotas:show": [], "volume_extension:quotas:update_for_project": [["rule:admin_api"]],