diff --git a/api-ref/source/v2/samples/stores-list-detail-response.json b/api-ref/source/v2/samples/stores-list-detail-response.json index f377518f56..50f3065156 100644 --- a/api-ref/source/v2/samples/stores-list-detail-response.json +++ b/api-ref/source/v2/samples/stores-list-detail-response.json @@ -6,14 +6,41 @@ "description": "More expensive store with data redundancy", "default": true, "properties": { - "pool": "pool1" + "pool": "pool1", + "chunk_size": 65536, + "thin_provisioning": false } }, { "id":"cheap", "type": "file", "description": "Less expensive store for seldom-used images", - "properties": {} + "properties": { + "datadir": "fdir", + "chunk_size": 65536, + "thin_provisioning": false + } + }, + { + "id":"fast", + "type": "cinder", + "description": "Reasonably-priced fast store", + "properties": { + "volume_type": "volume1", + "use_multipath": false + } + }, + { + "id":"slow", + "type": "swift", + "description": "Entry-level store balancing price and speed", + "properties": { + "container": "container1", + "large_object_size": 52428, + "large_object_chunk_size": 204800 + } } + + ] } diff --git a/glance/api/v2/discovery.py b/glance/api/v2/discovery.py index c0aa477dd5..720dde8096 100644 --- a/glance/api/v2/discovery.py +++ b/glance/api/v2/discovery.py @@ -77,6 +77,54 @@ class InfoController(object): return {'stores': backends} + @staticmethod + def _get_rbd_properties(store_detail): + return { + 'chunk_size': store_detail.chunk_size, + 'pool': store_detail.pool, + 'thin_provisioning': store_detail.thin_provisioning + } + + @staticmethod + def _get_file_properties(store_detail): + return { + 'data_dir': store_detail.datadir, + 'chunk_size': store_detail.chunk_size, + 'thin_provisioning': store_detail.thin_provisioning + } + + @staticmethod + def _get_cinder_properties(store_detail): + return { + 'volume_type': store_detail.store_conf.cinder_volume_type, + 'use_multipath': store_detail.store_conf.cinder_use_multipath + } + + @staticmethod + def _get_swift_properties(store_detail): + return { + 'container': store_detail.container, + 'large_object_size': store_detail.large_object_size, + 'large_object_chunk_size': store_detail.large_object_chunk_size + } + + @staticmethod + def _get_s3_properties(store_detail): + return { + 's3_store_large_object_size': + store_detail.s3_store_large_object_size, + 's3_store_large_object_chunk_size': + store_detail.s3_store_large_object_chunk_size, + 's3_store_thread_pools': + store_detail.s3_store_thread_pools + } + + @staticmethod + def _get_http_properties(store_detail): + # NOTE(mrjoshi): Thre are no useful properties + # to be exposed. + return {} + def get_stores_detail(self, req): enabled_backends = CONF.enabled_backends stores = self.get_stores(req).get('stores') @@ -84,17 +132,24 @@ class InfoController(object): api_policy.DiscoveryAPIPolicy( req.context, enforcer=self.policy).stores_info_detail() + + store_mapper = { + 'rbd': self._get_rbd_properties, + 'file': self._get_file_properties, + 'cinder': self._get_cinder_properties, + 'swift': self._get_swift_properties, + 's3': self._get_s3_properties, + 'http': self._get_http_properties + } + for store in stores: - store['type'] = enabled_backends[store['id']] - store['properties'] = {} - if store['type'] == 'rbd': - store_detail = g_store.get_store_from_store_identifier( - store['id']) - store['properties'] = {'chunk_size': - store_detail.chunk_size, - 'pool': store_detail.pool, - 'thin_provisioning': - store_detail.thin_provisioning} + store_type = enabled_backends[store['id']] + store['type'] = store_type + store_detail = g_store.get_store_from_store_identifier( + store['id']) + store['properties'] = store_mapper.get(store_type)( + store_detail) + except exception.Forbidden as e: LOG.debug("User not permitted to view details") raise webob.exc.HTTPForbidden(explanation=e.msg) diff --git a/glance/tests/functional/v2/test_discovery.py b/glance/tests/functional/v2/test_discovery.py index 537f19558e..026a9e1ba4 100644 --- a/glance/tests/functional/v2/test_discovery.py +++ b/glance/tests/functional/v2/test_discovery.py @@ -14,6 +14,7 @@ # under the License. import fixtures +import http.client as http from oslo_utils import units @@ -96,3 +97,68 @@ class TestDiscovery(functional.SynchronousAPIBase): expected['image_count_total']['usage'] = 1 expected['image_size_total']['usage'] = 1 self._assert_usage(expected) + + def test_stores(self): + # NOTE(mrjoshi): As this is a functional test, we are + # testing the functionality with file stores. + + self.start_server() + + # If user is admin or non-admin the store list will be + # displayed. + stores = self.api_get('/v2/info/stores').json['stores'] + expected = { + "stores": [ + { + "id": "store1", + "default": "true" + }, + { + "id": "store2" + }, + { + "id": "store3" + }]} + + self.assertEqual(expected['stores'], stores) + + # If user is admin the store list will be displayed + # along with store properties. + stores = self.api_get('/v2/info/stores/detail').json['stores'] + expected = { + "stores": [ + { + "id": "store1", + "default": "true", + "type": "file", + "properties": { + "data_dir": self._store_dir('store1'), + "chunk_size": 65536, + "thin_provisioning": False + } + }, + { + "id": "store2", + "type": "file", + "properties": { + "data_dir": self._store_dir('store2'), + "chunk_size": 65536, + "thin_provisioning": False + } + }, + { + "id": "store3", + "type": "file", + "properties": { + "data_dir": self._store_dir('store3'), + "chunk_size": 65536, + "thin_provisioning": False + } + }]} + + self.assertEqual(expected['stores'], stores) + + # If user is non-admin 403 Error response will be returned. + response = self.api_get('/v2/info/stores/detail', + headers={'X-Roles': 'member'}) + self.assertEqual(http.FORBIDDEN, response.status_code) diff --git a/glance/tests/unit/base.py b/glance/tests/unit/base.py index 2cdd8fa552..92aa28adb7 100644 --- a/glance/tests/unit/base.py +++ b/glance/tests/unit/base.py @@ -17,7 +17,9 @@ import os from unittest import mock import glance_store as store +from glance_store._drivers import cinder from glance_store._drivers import rbd as rbd_store +from glance_store._drivers import swift from glance_store import location from oslo_concurrency import lockutils from oslo_config import cfg @@ -74,21 +76,36 @@ class MultiStoreClearingUnitTest(test_utils.BaseTestCase): rbd_store.rados = mock.MagicMock() rbd_store.rbd = mock.MagicMock() rbd_store.Store._set_url_prefix = mock.MagicMock() + cinder.cinderclient = mock.MagicMock() + cinder.Store.get_cinderclient = mock.MagicMock() + swift.swiftclient = mock.MagicMock() + swift.BaseStore.get_store_connection = mock.MagicMock() self.config(enabled_backends={'fast': 'file', 'cheap': 'file', 'readonly_store': 'http', 'fast-cinder': 'cinder', - 'fast-rbd': 'rbd'}) + 'fast-rbd': 'rbd', 'reliable': 'swift'}) store.register_store_opts(CONF) self.config(default_backend='fast', group='glance_store') self.config(filesystem_store_datadir=self.test_dir, + filesystem_thin_provisioning=False, + filesystem_store_chunk_size=65536, group='fast') self.config(filesystem_store_datadir=self.test_dir2, + filesystem_thin_provisioning=False, + filesystem_store_chunk_size=65536, group='cheap') self.config(rbd_store_chunk_size=8688388, rbd_store_pool='images', rbd_thin_provisioning=False, group='fast-rbd') + self.config(cinder_volume_type='lvmdriver-1', + cinder_use_multipath=False, group='fast-cinder') + self.config(swift_store_container='glance', + swift_store_large_object_size=524288000, + swift_store_large_object_chunk_size=204800000, + group='reliable') + store.create_multi_stores(CONF) diff --git a/glance/tests/unit/v2/test_discovery_stores.py b/glance/tests/unit/v2/test_discovery_stores.py index 111064767c..6afdf751ae 100644 --- a/glance/tests/unit/v2/test_discovery_stores.py +++ b/glance/tests/unit/v2/test_discovery_stores.py @@ -40,7 +40,7 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest): def test_get_stores(self): available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder', - 'fast-rbd'] + 'fast-rbd', 'reliable'] req = unit_test_utils.get_fake_request() output = self.controller.get_stores(req) self.assertIn('stores', output) @@ -50,7 +50,7 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest): def test_get_stores_read_only_store(self): available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder', - 'fast-rbd'] + 'fast-rbd', 'reliable'] req = unit_test_utils.get_fake_request() output = self.controller.get_stores(req) self.assertIn('stores', output) @@ -77,22 +77,36 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest): def test_get_stores_detail(self): available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder', - 'fast-rbd'] - available_store_type = ['file', 'file', 'http', 'cinder', 'rbd'] + 'fast-rbd', 'reliable'] + available_store_type = ['file', 'file', 'http', 'cinder', 'rbd', + 'swift'] req = unit_test_utils.get_fake_request(roles=['admin']) output = self.controller.get_stores_detail(req) + self.assertEqual(len(CONF.enabled_backends), len(output['stores'])) self.assertIn('stores', output) for stores in output['stores']: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertIn(stores['type'], available_store_type) self.assertIsNotNone(stores['properties']) - if stores['id'] == 'fast-rbd': - self.assertIn('chunk_size', stores['properties']) - self.assertIn('pool', stores['properties']) - self.assertIn('thin_provisioning', stores['properties']) - else: - self.assertEqual({}, stores['properties']) + + def test_get_stores_detail_properties(self): + store_attributes = {'rbd': ['chunk_size', 'pool', 'thin_provisioning'], + 'file': ['data_dir', 'chunk_size', + 'thin_provisioning'], + 'cinder': ['volume_type', 'use_multipath'], + 'swift': ['container', + 'large_object_size', + 'large_object_chunk_size'], + 'http': []} + req = unit_test_utils.get_fake_request(roles=['admin']) + output = self.controller.get_stores_detail(req) + self.assertEqual(len(CONF.enabled_backends), len(output['stores'])) + self.assertIn('stores', output) + for store in output['stores']: + actual_attribute = list(store['properties'].keys()) + expected_attribute = store_attributes[store['type']] + self.assertEqual(actual_attribute, expected_attribute) def test_get_stores_detail_non_admin(self): req = unit_test_utils.get_fake_request() diff --git a/releasenotes/notes/expanding-stores-details-d3aa8ebb76ad68d9.yaml b/releasenotes/notes/expanding-stores-details-d3aa8ebb76ad68d9.yaml new file mode 100644 index 0000000000..5ca0796e57 --- /dev/null +++ b/releasenotes/notes/expanding-stores-details-d3aa8ebb76ad68d9.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + This release brings expansion in the functionality of + stores-detail API. The stores detail API will list the + way each store is configured, whereas previously this + worked only for rbd store. The API remains admin-only + by default as it exposes backend information.