]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add image metadata API extension
authorNikola Dipanov <ndipanov@redhat.com>
Fri, 7 Dec 2012 14:53:28 +0000 (15:53 +0100)
committerNikola Dipanov <ndipanov@redhat.com>
Fri, 14 Dec 2012 08:04:27 +0000 (09:04 +0100)
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

cinder/api/contrib/volume_image_metadata.py [new file with mode: 0644]
cinder/tests/api/contrib/test_volume_image_metadata.py [new file with mode: 0644]
cinder/tests/policy.json
cinder/volume/api.py
etc/cinder/policy.json

diff --git a/cinder/api/contrib/volume_image_metadata.py b/cinder/api/contrib/volume_image_metadata.py
new file mode 100644 (file)
index 0000000..58e5cd2
--- /dev/null
@@ -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 (file)
index 0000000..2c343cf
--- /dev/null
@@ -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)
index 3576076b84473e8b2e7729e1e71d0b0a31f00e94..a2c226f4bfd52e7a50c4a4bb965ed9543b9b913c 100644 (file)
@@ -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"]]
index 5a99c384a20e3ac04033e53c6165a07071c37f11..5c95233689d4e5aef7749f7ec17c158278a4f7e9 100644 (file)
@@ -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']:
index 9b088780a5c1096f5b3b791d62aaaa1f00d71047..7e917b7fa9ac02befa79f5fae08056d9f2e7e9d2 100644 (file)
@@ -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"]],