diff --git a/etc/search-policy.json b/etc/search-policy.json index abef5b3b0b..dc324e259f 100644 --- a/etc/search-policy.json +++ b/etc/search-policy.json @@ -3,5 +3,6 @@ "default": "", "catalog_index": "role:admin", - "catalog_search": "" + "catalog_search": "", + "catalog_plugins": "" } diff --git a/glance/api/policy.py b/glance/api/policy.py index e3c4d5710d..f7939cb247 100755 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -690,6 +690,10 @@ class CatalogSearchRepoProxy(object): self.policy.enforce(self.context, 'catalog_search', {}) return self.search_repo.search(*args, **kwargs) + def plugins_info(self, *args, **kwargs): + self.policy.enforce(self.context, 'catalog_plugins', {}) + return self.search_repo.plugins_info(*args, **kwargs) + def index(self, *args, **kwargs): self.policy.enforce(self.context, 'catalog_index', {}) return self.search_repo.index(*args, **kwargs) diff --git a/glance/common/utils.py b/glance/common/utils.py index 868235e481..1923be4117 100644 --- a/glance/common/utils.py +++ b/glance/common/utils.py @@ -32,6 +32,7 @@ import functools import os import platform import re +import stevedore import subprocess import sys import uuid @@ -729,3 +730,10 @@ def stash_conf_values(): conf['cert_file'] = CONF.cert_file return conf + + +def get_search_plugins(): + namespace = 'glance.search.index_backend' + ext_manager = stevedore.extension.ExtensionManager( + namespace, invoke_on_load=True) + return ext_manager.extensions diff --git a/glance/search/__init__.py b/glance/search/__init__.py index 5b36d7d578..e1cacf01f2 100644 --- a/glance/search/__init__.py +++ b/glance/search/__init__.py @@ -17,6 +17,8 @@ import elasticsearch from elasticsearch import helpers from oslo_config import cfg +from glance.common import utils + search_opts = [ cfg.ListOpt('hosts', default=['127.0.0.1:9200'], @@ -40,6 +42,8 @@ class CatalogSearchRepo(object): def __init__(self, context, es_api): self.context = context self.es_api = es_api + self.plugins = utils.get_search_plugins() or [] + self.plugins_info_dict = self._get_plugin_info() def search(self, index, doc_type, query, fields, offset, limit, ignore_unavailable=True): @@ -58,3 +62,16 @@ class CatalogSearchRepo(object): index=default_index, doc_type=default_type, actions=actions) + + def plugins_info(self): + return self.plugins_info_dict + + def _get_plugin_info(self): + plugin_info = dict() + plugin_info['plugins'] = [] + for plugin in self.plugins: + info = dict() + info['type'] = plugin.obj.get_document_type() + info['index'] = plugin.obj.get_index_name() + plugin_info['plugins'].append(info) + return plugin_info diff --git a/glance/search/api/v0_1/router.py b/glance/search/api/v0_1/router.py index ad0462a6fa..1f08b33b45 100755 --- a/glance/search/api/v0_1/router.py +++ b/glance/search/api/v0_1/router.py @@ -41,6 +41,17 @@ class API(wsgi.Router): conditions={'method': ['PUT', 'DELETE', 'PATCH', 'HEAD']}) + mapper.connect('/search/plugins', + controller=search_catalog_resource, + action='plugins_info', + conditions={'method': ['GET']}) + mapper.connect('/search/plugins', + controller=reject_method_resource, + action='reject', + allowed_methods='GET', + conditions={'method': ['POST', 'PUT', 'DELETE', + 'PATCH', 'HEAD']}) + mapper.connect('/index', controller=search_catalog_resource, action='index', diff --git a/glance/search/api/v0_1/search.py b/glance/search/api/v0_1/search.py index 64ca7ba414..f6aa4bf55d 100755 --- a/glance/search/api/v0_1/search.py +++ b/glance/search/api/v0_1/search.py @@ -18,7 +18,6 @@ import json from oslo.config import cfg from oslo_log import log as logging import six -import stevedore import webob.exc from glance.api import policy @@ -76,6 +75,18 @@ class SearchController(object): LOG.error(utils.exception_to_str(e)) raise webob.exc.HTTPInternalServerError() + def plugins_info(self, req): + try: + search_repo = self.gateway.get_catalog_search_repo(req.context) + return search_repo.plugins_info() + except exception.Forbidden as e: + raise webob.exc.HTTPForbidden(explanation=e.msg) + except exception.NotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.msg) + except Exception as e: + LOG.error(utils.exception_to_str(e)) + raise webob.exc.HTTPInternalServerError() + def index(self, req, actions, default_index=None, default_type=None): try: search_repo = self.gateway.get_catalog_search_repo(req.context) @@ -351,22 +362,20 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): response.unicode_body = six.text_type(body) response.content_type = 'application/json' + def plugins_info(self, response, query_result): + body = json.dumps(query_result, ensure_ascii=False) + response.unicode_body = six.text_type(body) + response.content_type = 'application/json' + def index(self, response, query_result): body = json.dumps(query_result, ensure_ascii=False) response.unicode_body = six.text_type(body) response.content_type = 'application/json' -def get_plugins(): - namespace = 'glance.search.index_backend' - ext_manager = stevedore.extension.ExtensionManager( - namespace, invoke_on_load=True) - return ext_manager.extensions - - def create_resource(): """Search resource factory method""" - plugins = get_plugins() + plugins = utils.get_search_plugins() deserializer = RequestDeserializer(plugins) serializer = ResponseSerializer() controller = SearchController(plugins) diff --git a/glance/tests/unit/v0_1/test_search.py b/glance/tests/unit/v0_1/test_search.py index d5782a9669..09a083cf6a 100755 --- a/glance/tests/unit/v0_1/test_search.py +++ b/glance/tests/unit/v0_1/test_search.py @@ -18,6 +18,7 @@ from oslo.serialization import jsonutils import webob.exc from glance.common import exception +from glance.common import utils import glance.gateway import glance.search from glance.search.api.v0_1 import search as search @@ -234,12 +235,52 @@ class TestSearchController(base.IsolatedUnitTest): webob.exc.HTTPInternalServerError, self.search_controller.index, request, actions) + def test_plugins_info(self): + request = unit_test_utils.get_fake_request() + self.search_controller.plugins_info = mock.Mock(return_value="{}") + self.search_controller.plugins_info(request) + self.search_controller.plugins_info.assert_called_once_with(request) + + def test_plugins_info_repo(self): + request = unit_test_utils.get_fake_request() + repo = glance.search.CatalogSearchRepo + repo.plugins_info = mock.Mock(return_value="{}") + self.search_controller.plugins_info(request) + repo.plugins_info.assert_called_once_with() + + def test_plugins_info_forbidden(self): + request = unit_test_utils.get_fake_request() + repo = glance.search.CatalogSearchRepo + repo.plugins_info = mock.Mock(side_effect=exception.Forbidden) + + self.assertRaises( + webob.exc.HTTPForbidden, self.search_controller.plugins_info, + request) + + def test_plugins_info_not_found(self): + request = unit_test_utils.get_fake_request() + repo = glance.search.CatalogSearchRepo + repo.plugins_info = mock.Mock(side_effect=exception.NotFound) + + self.assertRaises(webob.exc.HTTPNotFound, + self.search_controller.plugins_info, request) + + def test_plugins_info_internal_server_error(self): + request = unit_test_utils.get_fake_request() + repo = glance.search.CatalogSearchRepo + repo.plugins_info = mock.Mock(side_effect=Exception) + + self.assertRaises(webob.exc.HTTPInternalServerError, + self.search_controller.plugins_info, request) + class TestSearchDeserializer(test_utils.BaseTestCase): def setUp(self): super(TestSearchDeserializer, self).setUp() - self.deserializer = search.RequestDeserializer(search.get_plugins()) + self.deserializer = search.RequestDeserializer( + utils.get_search_plugins() + ) def test_single_index(self): request = unit_test_utils.get_fake_request() @@ -411,7 +452,9 @@ class TestIndexDeserializer(test_utils.BaseTestCase): def setUp(self): super(TestIndexDeserializer, self).setUp() - self.deserializer = search.RequestDeserializer(search.get_plugins()) + self.deserializer = search.RequestDeserializer( + utils.get_search_plugins() + ) def test_empty_request(self): request = unit_test_utils.get_fake_request() @@ -874,6 +917,39 @@ class TestResponseSerializer(test_utils.BaseTestCase): super(TestResponseSerializer, self).setUp() self.serializer = search.ResponseSerializer() + def test_plugins_info(self): + expected = { + "plugins": [ + { + "index": "glance", + "type": "image" + }, + { + "index": "glance", + "type": "metadef" + } + ] + } + + request = webob.Request.blank('/v0.1/search') + response = webob.Response(request=request) + result = { + "plugins": [ + { + "index": "glance", + "type": "image" + }, + { + "index": "glance", + "type": "metadef" + } + ] + } + self.serializer.search(response, result) + actual = jsonutils.loads(response.body) + self.assertEqual(expected, actual) + self.assertEqual('application/json', response.content_type) + def test_search(self): expected = [{ 'id': '1',