From d6d5c501c16ed7df64944f6646ad32aca345adcd Mon Sep 17 00:00:00 2001 From: Lakshmi N Sampath Date: Wed, 1 Apr 2015 08:38:15 -0700 Subject: [PATCH] Plugin types are not exposed to the client. Glance Search clients currently don't have anyway to know what "types" are available. Search clients need to know the "type" to include in the search request for the particular resource. Change-Id: I8baed7c5183752b1f52f8050ce321aacec701f1c Closes-Bug: #1439061 --- etc/search-policy.json | 3 +- glance/api/policy.py | 4 ++ glance/common/utils.py | 8 +++ glance/search/__init__.py | 17 ++++++ glance/search/api/v0_1/router.py | 11 ++++ glance/search/api/v0_1/search.py | 27 ++++++--- glance/tests/unit/v0_1/test_search.py | 80 ++++++++++++++++++++++++++- 7 files changed, 138 insertions(+), 12 deletions(-) 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',