Added a new API to expose store info

This patch adds a new API to glance ``GET /v2/info/stores/detail``
to expose the stores specific details about the store like store
type and other specific store properties.This operation
will be admin only and validated by the new policy rule
``stores_info_detail`` which defaults to admin only

Implements: blueprint expose-store-specific-info
Change-Id: I6882fd2381e6ae245fd8c61bf9f4d52df2b216f5
This commit is contained in:
Mridula Joshi 2022-01-12 13:01:15 +00:00 committed by Abhishek Kekane
parent 88c43d4715
commit a34764ecac
15 changed files with 252 additions and 16 deletions

View File

@ -18,3 +18,28 @@ stores:
in: body
required: true
type: array
stores-detail:
description: |
A list of store objects, where each store object may contain the
following fields:
``id``
Operator-defined identifier for the store.
``type``
Specify the type of store.
``description``
Operator-supplied description of this store.
``default`` (optional)
Only present on the default store. This is the store where image
data is placed if you do not indicate a specific store when supplying
data to the Image Service. (See the :ref:`Image data <image-data>`
and :ref:`Interoperable image import <image-import-process>` sections
for more information.)
``read-only`` (optional)
Included only when the store is read only.
``properties``
Contains store specific properties
in: body
required: true
type: array

View File

@ -126,3 +126,39 @@ Response Example
.. literalinclude:: samples/usage-response.json
:language: json
List stores detail
~~~~~~~~~~~~~~~~~~
.. rest_method:: GET /v2/info/stores/detail
Lists all the backend stores, with detail, accessible to admins,
for non-admin user API will return bad request.
Normal response codes: 200
Error response codes: 403, 404
Request
-------
There are no request parameters.
This call does not allow a request body.
Response Parameters
-------------------
.. rest_parameters:: discovery-parameters.yaml
- stores: stores-detail
Response Example
----------------
.. literalinclude:: samples/stores-list-detail-response.json
:language: json

View File

@ -0,0 +1,19 @@
{
"stores": [
{
"id":"reliable",
"type": "rbd",
"description": "More expensive store with data redundancy",
"default": true,
"properties": {
"pool": "pool1"
}
},
{
"id":"cheap",
"type": "file",
"description": "Less expensive store for seldom-used images",
"properties": {}
}
]
}

View File

@ -85,6 +85,7 @@ class VersionNegotiationFilter(wsgi.Middleware):
allowed_versions['v2.13'] = 2
if CONF.image_cache_dir:
allowed_versions['v2.14'] = 2
allowed_versions['v2.15'] = 2
if CONF.enabled_backends:
allowed_versions['v2.8'] = 2
allowed_versions['v2.10'] = 2

View File

@ -15,10 +15,15 @@
import copy
import glance_store as g_store
from oslo_config import cfg
from oslo_log import log as logging
import oslo_serialization.jsonutils as json
import webob.exc
from glance.api import policy
from glance.api.v2 import policy as api_policy
from glance.common import exception
from glance.common import wsgi
import glance.db
from glance.i18n import _
@ -26,8 +31,13 @@ from glance.quota import keystone as ks_quota
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class InfoController(object):
def __init__(self, policy_enforcer=None):
self.policy = policy_enforcer or policy.Enforcer()
def get_image_import(self, req):
# TODO(jokke): All the rest of the boundaries should be implemented.
import_methods = {
@ -67,6 +77,30 @@ class InfoController(object):
return {'stores': backends}
def get_stores_detail(self, req):
enabled_backends = CONF.enabled_backends
stores = self.get_stores(req).get('stores')
try:
api_policy.DiscoveryAPIPolicy(
req.context,
enforcer=self.policy).stores_info_detail()
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}
except exception.Forbidden as e:
LOG.debug("User not permitted to view details")
raise webob.exc.HTTPForbidden(explanation=e.msg)
return {'stores': stores}
def get_usage(self, req):
project_usage = ks_quota.get_usage(req.context)
return {'usage':

View File

@ -122,6 +122,17 @@ class CacheImageAPIPolicy(APIPolicyBase):
self._enforce(self.policy_str)
class DiscoveryAPIPolicy(APIPolicyBase):
def __init__(self, context, target=None, enforcer=None):
self._context = context
self._target = target or {}
self.enforcer = enforcer or policy.Enforcer()
super(DiscoveryAPIPolicy, self).__init__(context, target, enforcer)
def stores_info_detail(self):
self._enforce('stores_info_detail')
class ImageAPIPolicy(APIPolicyBase):
def __init__(self, context, image, enforcer=None):
"""Image API policy module.

View File

@ -593,6 +593,15 @@ class API(wsgi.Router):
controller=info_resource,
action='get_usage',
conditions={'method': ['GET']})
mapper.connect('/info/stores/detail',
controller=info_resource,
action='get_stores_detail',
conditions={'method': ['GET']},
body_reject=True)
mapper.connect('/info/stores/detail',
controller=reject_method_resource,
action='reject',
allowed_methods='GET')
# Cache Management API
cache_manage_resource = cached_images.create_resource()

View File

@ -79,15 +79,16 @@ class Controller(object):
version_objs = []
if CONF.image_cache_dir:
version_objs.extend([
build_version_object(2.14, 'v2', 'CURRENT'),
build_version_object(2.13, 'v2', 'SUPPORTED'),
build_version_object(2.15, 'v2', 'CURRENT'),
build_version_object(2.14, 'v2', 'SUPPORTED'),
])
else:
version_objs.extend([
build_version_object(2.13, 'v2', 'CURRENT'),
build_version_object(2.15, 'v2', 'CURRENT'),
])
if CONF.enabled_backends:
version_objs.extend([
build_version_object(2.13, 'v2', 'SUPPORTED'),
build_version_object(2.12, 'v2', 'SUPPORTED'),
build_version_object(2.11, 'v2', 'SUPPORTED'),
build_version_object('2.10', 'v2', 'SUPPORTED'),

View File

@ -14,6 +14,7 @@ import itertools
from glance.policies import base
from glance.policies import cache
from glance.policies import discovery
from glance.policies import image
from glance.policies import metadef
from glance.policies import tasks
@ -26,4 +27,5 @@ def list_rules():
tasks.list_rules(),
metadef.list_rules(),
cache.list_rules(),
discovery.list_rules(),
)

View File

@ -0,0 +1,33 @@
# Copyright 2021 Red Hat, Inc.
# 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 oslo_policy import policy
discovery_policies = [
policy.DocumentedRuleDefault(
name="stores_info_detail",
check_str='role:admin',
scope_types=['system', 'project'],
description='Expose store specific information',
operations=[
{'path': '/v2/info/stores/detail',
'method': 'GET'}
]
),
]
def list_rules():
return discovery_policies

View File

@ -17,6 +17,7 @@ import os
from unittest import mock
import glance_store as store
from glance_store._drivers import rbd as rbd_store
from glance_store import location
from oslo_concurrency import lockutils
from oslo_config import cfg
@ -70,9 +71,13 @@ class MultiStoreClearingUnitTest(test_utils.BaseTestCase):
:param passing_config: making store driver passes basic configurations.
:returns: the number of how many store drivers been loaded.
"""
rbd_store.rados = mock.MagicMock()
rbd_store.rbd = mock.MagicMock()
rbd_store.Store._set_url_prefix = mock.MagicMock()
self.config(enabled_backends={'fast': 'file', 'cheap': 'file',
'readonly_store': 'http',
'fast-cinder': 'cinder'})
'fast-cinder': 'cinder',
'fast-rbd': 'rbd'})
store.register_store_opts(CONF)
self.config(default_backend='fast',
@ -82,6 +87,8 @@ class MultiStoreClearingUnitTest(test_utils.BaseTestCase):
group='fast')
self.config(filesystem_store_datadir=self.test_dir2,
group='cheap')
self.config(rbd_store_chunk_size=8688388, rbd_store_pool='images',
rbd_thin_provisioning=False, group='fast-rbd')
store.create_multi_stores(CONF)

View File

@ -32,7 +32,7 @@ def get_versions_list(url, enabled_backends=False,
enabled_cache=False):
image_versions = [
{
'id': 'v2.13',
'id': 'v2.15',
'status': 'CURRENT',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
@ -95,11 +95,17 @@ def get_versions_list(url, enabled_backends=False,
if enabled_backends:
image_versions = [
{
'id': 'v2.13',
'id': 'v2.15',
'status': 'CURRENT',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
},
{
'id': 'v2.13',
'status': 'SUPPORTED',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
},
{
'id': 'v2.12',
'status': 'SUPPORTED',
@ -133,13 +139,12 @@ def get_versions_list(url, enabled_backends=False,
] + image_versions[2:]
if enabled_cache:
image_versions.insert(0, {
image_versions.insert(1, {
'id': 'v2.14',
'status': 'CURRENT',
'status': 'SUPPORTED',
'links': [{'rel': 'self',
'href': '%s/v2/' % url}],
})
image_versions[1]['status'] = 'SUPPORTED'
return image_versions
@ -393,15 +398,20 @@ class VersionNegotiationTest(base.IsolatedUnitTest):
self.middleware.process_request(request)
self.assertEqual('/v2/images', request.path_info)
# version 2.15 does not exist
def test_request_url_v2_15_default_unsupported(self):
def test_request_url_v2_15_enabled_supported(self):
request = webob.Request.blank('/v2.15/images')
self.middleware.process_request(request)
self.assertEqual('/v2/images', request.path_info)
# version 2.16 does not exist
def test_request_url_v2_16_default_unsupported(self):
request = webob.Request.blank('/v2.16/images')
resp = self.middleware.process_request(request)
self.assertIsInstance(resp, versions.Controller)
def test_request_url_v2_15_enabled_unsupported(self):
self.config(image_cache_dir='/tmp/cache')
request = webob.Request.blank('/v2.15/images')
def test_request_url_v2_16_enabled_unsupported(self):
self.config(enabled_backends='slow:one,fast:two')
request = webob.Request.blank('/v2.16/images')
resp = self.middleware.process_request(request)
self.assertIsInstance(resp, versions.Controller)

View File

@ -39,7 +39,8 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
req)
def test_get_stores(self):
available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder']
available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder',
'fast-rbd']
req = unit_test_utils.get_fake_request()
output = self.controller.get_stores(req)
self.assertIn('stores', output)
@ -48,7 +49,8 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
self.assertIn(stores['id'], available_stores)
def test_get_stores_read_only_store(self):
available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder']
available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder',
'fast-rbd']
req = unit_test_utils.get_fake_request()
output = self.controller.get_stores(req)
self.assertIn('stores', output)
@ -72,3 +74,28 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
self.assertEqual(2, len(output['stores']))
for stores in output["stores"]:
self.assertFalse(stores["id"].startswith("os_glance_"))
def test_get_stores_detail(self):
available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder',
'fast-rbd']
available_store_type = ['file', 'file', 'http', 'cinder', 'rbd']
req = unit_test_utils.get_fake_request(roles=['admin'])
output = self.controller.get_stores_detail(req)
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_non_admin(self):
req = unit_test_utils.get_fake_request()
self.assertRaises(webob.exc.HTTPForbidden,
self.controller.get_stores_detail,
req)

View File

@ -821,3 +821,18 @@ class TestCacheImageAPIPolicy(utils.BaseTestCase):
self.enforcer.enforce.assert_called_once_with(self.context,
'cache_image',
mock.ANY)
class TestDiscoveryAPIPolicy(APIPolicyBase):
def setUp(self):
super(TestDiscoveryAPIPolicy, self).setUp()
self.enforcer = mock.MagicMock()
self.context = mock.MagicMock()
self.policy = policy.DiscoveryAPIPolicy(
self.context, enforcer=self.enforcer)
def test_stores_info_detail(self):
self.policy.stores_info_detail()
self.enforcer.enforce.assert_called_once_with(self.context,
'stores_info_detail',
mock.ANY)

View File

@ -0,0 +1,6 @@
---
features:
- |
This release brings additional functionality to the stores API.
The stores detail API helps in providing the store specific
information.