Merge "Added search resource client and cli"
This commit is contained in:
commit
22400222f1
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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', {})
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue