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:
parent
88c43d4715
commit
a34764ecac
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
19
api-ref/source/v2/samples/stores-list-detail-response.json
Normal file
19
api-ref/source/v2/samples/stores-list-detail-response.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
|
@ -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':
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
|
@ -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(),
|
||||
)
|
||||
|
33
glance/policies/discovery.py
Normal file
33
glance/policies/discovery.py
Normal 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
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user