From 826c4520d093f5e0809a08e10b5d18bf69adc698 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 27 Oct 2012 15:36:30 -0700 Subject: [PATCH] Add VolumeHostAttribute API extension Expose the host on which a volume resides through a new API extension. This is only presented to Admins by default. This can be controlled with the 'volume_host_attribute' policy rule. Fixes bug 1035350. Change-Id: I0a74a0dfbd78e853219150fbe0d3fba77c6f9bb6 --- .../volume/contrib/volume_host_attribute.py | 93 +++++++++++++ .../contrib/test_volume_host_attribute.py | 131 ++++++++++++++++++ cinder/tests/policy.json | 3 +- etc/cinder/policy.json | 4 +- 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 cinder/api/openstack/volume/contrib/volume_host_attribute.py create mode 100644 cinder/tests/api/openstack/volume/contrib/test_volume_host_attribute.py diff --git a/cinder/api/openstack/volume/contrib/volume_host_attribute.py b/cinder/api/openstack/volume/contrib/volume_host_attribute.py new file mode 100644 index 000000000..5db3c18ef --- /dev/null +++ b/cinder/api/openstack/volume/contrib/volume_host_attribute.py @@ -0,0 +1,93 @@ +# 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. + +from cinder.api.openstack import extensions +from cinder.api.openstack import wsgi +from cinder.api.openstack import xmlutil +from cinder.openstack.common import log as logging +from cinder import volume + + +LOG = logging.getLogger(__name__) +authorize = extensions.soft_extension_authorizer('volume', + 'volume_host_attribute') + + +class VolumeHostAttributeController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(VolumeHostAttributeController, self).__init__(*args, **kwargs) + self.volume_api = volume.API() + + def _add_volume_host_attribute(self, context, resp_volume): + try: + db_volume = self.volume_api.get(context, resp_volume['id']) + except Exception: + return + else: + key = "%s:host" % Volume_host_attribute.alias + resp_volume[key] = db_volume['host'] + + @wsgi.extends + def show(self, req, resp_obj, id): + context = req.environ['cinder.context'] + if authorize(context): + resp_obj.attach(xml=VolumeHostAttributeTemplate()) + self._add_volume_host_attribute(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=VolumeListHostAttributeTemplate()) + for volume in list(resp_obj.obj['volumes']): + self._add_volume_host_attribute(context, volume) + + +class Volume_host_attribute(extensions.ExtensionDescriptor): + """Expose host as an attribute of a volume.""" + + name = "VolumeHostAttribute" + alias = "os-vol-host-attr" + namespace = ("http://docs.openstack.org/volume/ext/" + "volume_host_attribute/api/v1") + updated = "2011-11-03T00:00:00+00:00" + + def get_controller_extensions(self): + controller = VolumeHostAttributeController() + extension = extensions.ControllerExtension(self, 'volumes', controller) + return [extension] + + +def make_volume(elem): + elem.set('{%s}host' % Volume_host_attribute.namespace, + '%s:host' % Volume_host_attribute.alias) + + +class VolumeHostAttributeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + alias = Volume_host_attribute.alias + namespace = Volume_host_attribute.namespace + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) + + +class VolumeListHostAttributeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + alias = Volume_host_attribute.alias + namespace = Volume_host_attribute.namespace + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) diff --git a/cinder/tests/api/openstack/volume/contrib/test_volume_host_attribute.py b/cinder/tests/api/openstack/volume/contrib/test_volume_host_attribute.py new file mode 100644 index 000000000..ed878b415 --- /dev/null +++ b/cinder/tests/api/openstack/volume/contrib/test_volume_host_attribute.py @@ -0,0 +1,131 @@ +# 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 json +import datetime + +from lxml import etree +import webob + +from cinder import context +from cinder import test +from cinder import utils +from cinder import volume +from cinder.tests.api.openstack import fakes + + +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, + } + + +def fake_volume_get_all(*args, **kwargs): + return [fake_volume_get()] + + +def app(): + # no auth, just let environ['cinder.context'] pass through + api = fakes.volume.APIRouter() + mapper = fakes.urlmap.URLMap() + mapper['/v1'] = api + return mapper + + +class VolumeHostAttributeTest(test.TestCase): + + def setUp(self): + super(VolumeHostAttributeTest, self).setUp() + self.stubs.Set(volume.API, 'get', fake_volume_get) + self.stubs.Set(volume.API, 'get_all', fake_volume_get_all) + self.UUID = utils.gen_uuid() + + def test_get_volume_allowed(self): + ctx = context.RequestContext('admin', 'fake', True) + req = webob.Request.blank('/v1/fake/volumes/%s' % self.UUID) + req.method = 'GET' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = json.loads(res.body)['volume'] + self.assertEqual(vol['os-vol-host-attr:host'], 'host001') + + def test_get_volume_unallowed(self): + ctx = context.RequestContext('non-admin', 'fake', False) + req = webob.Request.blank('/v1/fake/volumes/%s' % self.UUID) + req.method = 'GET' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = json.loads(res.body)['volume'] + self.assertFalse('os-vol-host-attr:host' in vol) + + def test_list_detail_volumes_allowed(self): + ctx = context.RequestContext('admin', 'fake', True) + req = webob.Request.blank('/v1/fake/volumes/detail') + req.method = 'GET' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = json.loads(res.body)['volumes'] + self.assertEqual(vol[0]['os-vol-host-attr:host'], 'host001') + + def test_list_detail_volumes_unallowed(self): + ctx = context.RequestContext('non-admin', 'fake', False) + req = webob.Request.blank('/v1/fake/volumes/detail') + req.method = 'GET' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = json.loads(res.body)['volumes'] + self.assertFalse('os-vol-host-attr:host' in vol[0]) + + def test_list_simple_volumes_no_host(self): + ctx = context.RequestContext('admin', 'fake', True) + req = webob.Request.blank('/v1/fake/volumes') + req.method = 'GET' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = json.loads(res.body)['volumes'] + self.assertFalse('os-vol-host-attr:host' in vol[0]) + + def test_get_volume_xml(self): + ctx = context.RequestContext('admin', 'fake', True) + req = webob.Request.blank('/v1/fake/volumes/%s' % self.UUID) + req.method = 'GET' + req.accept = 'application/xml' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = etree.XML(res.body) + host_key = ('{http://docs.openstack.org/volume/ext/' + 'volume_host_attribute/api/v1}host') + self.assertEqual(vol.get(host_key), 'host001') + + def test_list_volumes_detail_xml(self): + ctx = context.RequestContext('admin', 'fake', True) + req = webob.Request.blank('/v1/fake/volumes/detail') + req.method = 'GET' + req.accept = 'application/xml' + req.environ['cinder.context'] = ctx + res = req.get_response(app()) + vol = list(etree.XML(res.body))[0] + host_key = ('{http://docs.openstack.org/volume/ext/' + 'volume_host_attribute/api/v1}host') + self.assertEqual(vol.get(host_key), 'host001') diff --git a/cinder/tests/policy.json b/cinder/tests/policy.json index 80a1913cf..dd0f80eef 100644 --- a/cinder/tests/policy.json +++ b/cinder/tests/policy.json @@ -33,5 +33,6 @@ "volume_extension:volume_actions:upload_image": [], "volume_extension:types_manage": [], "volume_extension:types_extra_specs": [], - "volume_extension:extended_snapshot_attributes": [] + "volume_extension:extended_snapshot_attributes": [], + "volume_extension:volume_host_attribute": [["rule:admin_api"]] } diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 9b3485e0c..96c01a554 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -23,5 +23,7 @@ "volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:volume_admin_actions:force_delete": [["rule:admin_api"]], - "volume_extension:snapshot_admin_actions:force_delete": [["rule:admin_api"]] + "volume_extension:snapshot_admin_actions:force_delete": [["rule:admin_api"]], + + "volume_extension:volume_host_attribute": [["rule:admin_api"]] } -- 2.45.2