Merge "Added search resource client and cli"

This commit is contained in:
Jenkins 2016-01-22 17:23:31 +00:00 committed by Gerrit Code Review
commit 22400222f1
7 changed files with 334 additions and 0 deletions

View File

@ -0,0 +1,97 @@
# 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.
#
"""Searchlight v1 Search action implementations"""
import logging
import six
from cliff import lister
from openstackclient.common import utils
class SearchResource(lister.Lister):
"""Search Searchlight resource."""
log = logging.getLogger(__name__ + ".SearchResource")
def get_parser(self, prog_name):
parser = super(SearchResource, self).get_parser(prog_name)
parser.add_argument(
"query",
metavar="<query>",
help="Query resources by Elasticsearch query string "
"(NOTE: json format DSL is not supported yet). "
"Example: 'name: cirros AND updated_at: [now-1y TO now]'. "
"See Elasticsearch DSL or Searchlight documentation for "
"more detail."
)
parser.add_argument(
"--type",
nargs='*',
metavar="<resource-type>",
help="One or more types to search. Uniquely identifies resource "
"types. Example: --type OS::Glance::Image "
"OS::Nova::Server"
)
parser.add_argument(
"--all-projects",
action='store_true',
default=False,
help="By default searches are restricted to the current project "
"unless all_projects is set"
)
parser.add_argument(
"--source",
action='store_true',
default=False,
help="Whether to display the source details, defaults to false. "
"You can specify --max-width to make the output look better."
)
return parser
@utils.log_method(log)
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
search_client = self.app.client_manager.search
mapping = {"_score": "score", "_type": "type", "_id": "id",
"_index": "index", "_source": "source"}
params = {
"type": parsed_args.type,
"all_projects": parsed_args.all_projects
}
if parsed_args.source:
columns = ("ID", "Score", "Type", "Source")
else:
columns = ("ID", "Name", "Score", "Type", "Updated")
# Only return the required fields when source not specified.
params["_source"] = ["name", "updated_at"]
# TODO(lyj): json should be supported for query
if parsed_args.query:
query = {"query_string": {"query": parsed_args.query}}
params['query'] = query
data = search_client.search.search(**params)
result = []
for r in data.hits['hits']:
converted = {}
for k, v in six.iteritems(r):
converted[mapping[k]] = v
if k == "_source" and not parsed_args.source:
converted["name"] = v.get("name")
converted["updated"] = v.get("updated_at")
result.append(utils.get_dict_properties(converted, columns))
return (columns, result)

View File

@ -35,6 +35,23 @@ Facet = {
}
Resource = {
"hits":
{"hits":
[
{"_score": 0.3, "_type": "OS::Glance::Image", "_id": "1",
"_source": {"name": "image1",
"updated_at": "2016-01-01T00:00:00Z"}},
{"_score": 0.3, "_type": "OS::Nova::Server", "_id": "2",
"_source": {"name": "instance1",
"updated_at": "2016-01-01T00:00:00Z"}},
],
"_shards": {"successful": 5, "failed": 0, "total": 5},
"took": 5, "timed_out": False
}
}
class FakeSearchv1Client(object):
def __init__(self, **kwargs):
self.http_client = mock.Mock()
@ -44,6 +61,8 @@ class FakeSearchv1Client(object):
self.resource_types.list = mock.Mock(return_value=[])
self.facets = mock.Mock()
self.facets.list = mock.Mock(return_value=[])
self.search = mock.Mock()
self.search.search = mock.Mock(return_value=[])
class TestSearchv1(utils.TestCommand):

View File

@ -0,0 +1,91 @@
# 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.
#
import copy
from searchlightclient.osc.v1 import search
from searchlightclient.tests.unit.osc import fakes
from searchlightclient.tests.unit.osc.v1 import fakes as searchlight_fakes
class TestSearch(searchlight_fakes.TestSearchv1):
def setUp(self):
super(TestSearch, self).setUp()
self.search_client = self.app.client_manager.search.search
class TestSearchResource(TestSearch):
def setUp(self):
super(TestSearchResource, self).setUp()
self.cmd = search.SearchResource(self.app, None)
self.search_client.search.return_value = \
fakes.FakeResource(
None,
copy.deepcopy(searchlight_fakes.Resource),
loaded=True)
def _test_search(self, arglist, **assertArgs):
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
details = False
if assertArgs.get('source'):
details = assertArgs.pop('source')
self.search_client.search.assert_called_with(**assertArgs)
if details:
collist = ("ID", "Score", "Type", "Source")
datalist = (
('1', 0.3, 'OS::Glance::Image',
{'name': 'image1', 'updated_at': '2016-01-01T00:00:00Z'}),
('2', 0.3, 'OS::Nova::Server',
{'name': 'instance1', 'updated_at': '2016-01-01T00:00:00Z'}))
else:
collist = ("ID", "Name", "Score", "Type", "Updated")
datalist = (('1', 'image1', 0.3, 'OS::Glance::Image',
'2016-01-01T00:00:00Z'),
('2', 'instance1', 0.3, 'OS::Nova::Server',
'2016-01-01T00:00:00Z'))
self.assertEqual(collist, columns)
self.assertEqual(datalist, tuple(data))
def test_search(self):
self._test_search(["name: fake"],
query={"query_string": {"query": "name: fake"}},
_source=['name', 'updated_at'],
all_projects=False, type=None)
def test_search_resource(self):
self._test_search(["name: fake", "--type", "res1", "res2"],
query={"query_string": {"query": "name: fake"}},
_source=['name', 'updated_at'],
type=["res1", "res2"],
all_projects=False)
def test_search_query_only(self):
self._test_search(["name: fake"],
query={"query_string": {"query": "name: fake"}},
_source=['name', 'updated_at'],
all_projects=False, type=None)
def test_list_all_projects(self):
self._test_search(["name: fake", "--all-projects"],
query={"query_string": {"query": "name: fake"}},
_source=['name', 'updated_at'],
all_projects=True, type=None)
def test_list_source(self):
self._test_search(["name: fake", "--source"],
query={"query_string": {"query": "name: fake"}},
all_projects=False, source=True, type=None)

View File

@ -0,0 +1,73 @@
# 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 searchlightclient.v1 import search
import mock
import testtools
class SearchManagerTest(testtools.TestCase):
def setUp(self):
super(SearchManagerTest, self).setUp()
self.manager = search.SearchManager(None)
self.manager._post = mock.MagicMock()
def test_search_with_query(self):
query_string = {
'query_string': {
'query': 'database'
}
}
self.manager.search(query=query_string)
self.manager._post.assert_called_once_with(
'/v1/search', {'query': query_string})
def test_search_with_type(self):
self.manager.search(type='fake_type')
self.manager._post.assert_called_once_with(
'/v1/search', {'type': 'fake_type'})
def test_search_with_offset(self):
self.manager.search(offset='fake_offset')
self.manager._post.assert_called_once_with(
'/v1/search', {'offset': 'fake_offset'})
def test_search_with_limit(self):
self.manager.search(limit=10)
self.manager._post.assert_called_once_with(
'/v1/search', {'limit': 10})
def test_search_with_sort(self):
self.manager.search(sort='asc')
self.manager._post.assert_called_once_with(
'/v1/search', {'sort': 'asc'})
def test_search_with_source(self):
self.manager.search(_source=['fake_source'])
self.manager._post.assert_called_once_with(
'/v1/search', {'_source': ['fake_source']})
def test_search_with_highlight(self):
self.manager.search(highlight='fake_highlight')
self.manager._post.assert_called_once_with(
'/v1/search', {'highlight': 'fake_highlight'})
def test_search_with_all_projects(self):
self.manager.search(all_projects=True)
self.manager._post.assert_called_once_with(
'/v1/search', {'all_projects': True})
def test_search_with_invalid_option(self):
self.manager.search(invalid='fake')
self.manager._post.assert_called_once_with('/v1/search', {})

View File

@ -13,6 +13,7 @@
from searchlightclient import client
from searchlightclient.v1 import facets
from searchlightclient.v1 import resource_types
from searchlightclient.v1 import search
class Client(object):
@ -38,3 +39,4 @@ class Client(object):
self.resource_types = resource_types.ResourceTypeManager(
self.http_client)
self.facets = facets.FacetsManager(self.http_client)
self.search = search.SearchManager(self.http_client)

View File

@ -0,0 +1,51 @@
# 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.
import six
from searchlightclient.openstack.common.apiclient import base
class Search(base.Resource):
def __repr__(self):
return "<Resource %s>" % self._info
def search(self, **kwargs):
return self.manager.search(self, **kwargs)
class SearchManager(base.BaseManager):
resource_class = Search
def search(self, **kwargs):
"""Executes a search query against searchlight and returns the 'hits'
from the response. Currently accepted parameters are (all optional):
:param query: see Elasticsearch DSL or Searchlight documentation;
defaults to match everything
:param type: one or more types to search. Uniquely identifies resource
types. Example: OS::Glance::Image
:param offset: skip over this many results
:param limit: return this many results
:param sort: sort by one or more fields
:param _source: restrict the fields returned for each document
:param highlight: add an Elasticsearch highlight clause
:param all_projects: by default searches are restricted to the
current project unless all_projects is set
"""
search_params = {}
for k, v in six.iteritems(kwargs):
if k in ('query', 'type', 'offset',
'limit', 'sort', '_source', 'highlight', 'all_projects'):
search_params[k] = v
resources = self._post('/v1/search', search_params)
return resources

View File

@ -29,6 +29,7 @@ openstack.cli.extension =
openstack.search.v1 =
search_resource_type_list = searchlightclient.osc.v1.resource_type:ListResourceType
search_facet_list = searchlightclient.osc.v1.facet:ListFacet
search_query = searchlightclient.osc.v1.search:SearchResource
[global]
setup-hooks =