Allow search for resource
This commit is contained in:
@@ -63,7 +63,7 @@ class ResourceClientTest(base.ClientTestBase):
|
||||
self.assertEqual(self.PROJECT_ID, resource_updated["project_id"])
|
||||
self.assertEqual(resource["started_at"],
|
||||
resource_updated["started_at"])
|
||||
self.assertEqual("temperature", resource["metrics"].keys()[0])
|
||||
self.assertIn("temperature", resource_updated["metrics"])
|
||||
|
||||
# GET
|
||||
result = self.gnocchi(
|
||||
@@ -83,6 +83,15 @@ class ResourceClientTest(base.ClientTestBase):
|
||||
self.assertEqual(self.PROJECT_ID, resource_list["project_id"])
|
||||
self.assertEqual(resource["started_at"], resource_list["started_at"])
|
||||
|
||||
# Search
|
||||
result = self.gnocchi('resource',
|
||||
params="search generic --query 'project_id=%s'" %
|
||||
self.PROJECT_ID)
|
||||
resource_list = self.parser.listing(result)[0]
|
||||
self.assertEqual(self.RESOURCE_ID, resource_list["id"])
|
||||
self.assertEqual(self.PROJECT_ID, resource_list["project_id"])
|
||||
self.assertEqual(resource["started_at"], resource_list["started_at"])
|
||||
|
||||
# DELETE
|
||||
result = self.gnocchi('resource',
|
||||
params="delete %s" % self.RESOURCE_ID)
|
||||
|
||||
83
gnocchiclient/tests/unit/test_utils.py
Normal file
83
gnocchiclient/tests/unit/test_utils.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 oslotest import base
|
||||
|
||||
from gnocchiclient import utils
|
||||
|
||||
|
||||
class SearchQueryBuilderTest(base.BaseTestCase):
|
||||
def _do_test(self, expr, expected):
|
||||
req = utils.search_query_builder(expr)
|
||||
self.assertEqual(expected, req)
|
||||
|
||||
def test_search_query_builder(self):
|
||||
self._do_test('foo=bar', {"=": {"foo": "bar"}})
|
||||
self._do_test('foo!=1', {"!=": {"foo": 1.0}})
|
||||
self._do_test('foo=True', {"=": {"foo": True}})
|
||||
self._do_test('foo=null', {"=": {"foo": None}})
|
||||
self._do_test('foo="null"', {"=": {"foo": "null"}})
|
||||
self._do_test('foo in ["null", "foo"]',
|
||||
{"in": {"foo": ["null", "foo"]}})
|
||||
self._do_test(u'foo="quote" and bar≠1',
|
||||
{"and": [{u"≠": {"bar": 1}},
|
||||
{"=": {"foo": "quote"}}]})
|
||||
self._do_test('foo="quote" or bar like "%%foo"',
|
||||
{"or": [{"like": {"bar": "%%foo"}},
|
||||
{"=": {"foo": "quote"}}]})
|
||||
|
||||
self._do_test('not (foo="quote" or bar like "%%foo" or foo="what!" '
|
||||
'or bar="who?")',
|
||||
{"not": {"or": [
|
||||
{"=": {"bar": "who?"}},
|
||||
{"=": {"foo": "what!"}},
|
||||
{"like": {"bar": "%%foo"}},
|
||||
{"=": {"foo": "quote"}},
|
||||
]}})
|
||||
|
||||
self._do_test('(foo="quote" or bar like "%%foo" or not foo="what!" '
|
||||
'or bar="who?") and cat="meme"',
|
||||
{"and": [
|
||||
{"=": {"cat": "meme"}},
|
||||
{"or": [
|
||||
{"=": {"bar": "who?"}},
|
||||
{"not": {"=": {"foo": "what!"}}},
|
||||
{"like": {"bar": "%%foo"}},
|
||||
{"=": {"foo": "quote"}},
|
||||
]}
|
||||
]})
|
||||
|
||||
self._do_test('foo="quote" or bar like "%%foo" or foo="what!" '
|
||||
'or bar="who?" and cat="meme"',
|
||||
{"or": [
|
||||
{"and": [
|
||||
{"=": {"cat": "meme"}},
|
||||
{"=": {"bar": "who?"}},
|
||||
]},
|
||||
{"=": {"foo": "what!"}},
|
||||
{"like": {"bar": "%%foo"}},
|
||||
{"=": {"foo": "quote"}},
|
||||
]})
|
||||
|
||||
self._do_test('foo="quote" or bar like "%%foo" and foo="what!" '
|
||||
'or bar="who?" or cat="meme"',
|
||||
{"or": [
|
||||
{"=": {"cat": "meme"}},
|
||||
{"=": {"bar": "who?"}},
|
||||
{"and": [
|
||||
{"=": {"foo": "what!"}},
|
||||
{"like": {"bar": "%%foo"}},
|
||||
]},
|
||||
{"=": {"foo": "quote"}},
|
||||
]})
|
||||
85
gnocchiclient/utils.py
Normal file
85
gnocchiclient/utils.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 pyparsing as pp
|
||||
|
||||
uninary_operators = ("not", )
|
||||
binary_operator = (u">=", u"<=", u"!=", u">", u"<", u"=", u"==", u"eq", u"ne",
|
||||
u"lt", u"gt", u"ge", u"le", u"in", u"like", u"≠", u"≥",
|
||||
u"≤", u"like" "in")
|
||||
multiple_operators = (u"and", u"or", u"∧", u"∨")
|
||||
|
||||
operator = pp.Regex(u"|".join(binary_operator))
|
||||
null = pp.Regex("None|none|null").setParseAction(pp.replaceWith(None))
|
||||
boolean = "False|True|false|true"
|
||||
boolean = pp.Regex(boolean).setParseAction(lambda t: t[0].lower() == "true")
|
||||
hex_string = lambda n: pp.Word(pp.hexnums, exact=n)
|
||||
uuid = pp.Combine(hex_string(8) + ("-" + hex_string(4)) * 3 +
|
||||
"-" + hex_string(12))
|
||||
number = r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?"
|
||||
number = pp.Regex(number).setParseAction(lambda t: float(t[0]))
|
||||
identifier = pp.Word(pp.alphas, pp.alphanums + "_")
|
||||
quoted_string = pp.QuotedString('"')
|
||||
comparison_term = pp.Forward()
|
||||
in_list = pp.Group(pp.Suppress('[') +
|
||||
pp.Optional(pp.delimitedList(comparison_term)) +
|
||||
pp.Suppress(']'))("list")
|
||||
comparison_term << (null | boolean | uuid | identifier | number |
|
||||
quoted_string | in_list)
|
||||
condition = pp.Group(comparison_term + operator + comparison_term)
|
||||
|
||||
expr = pp.operatorPrecedence(condition, [
|
||||
("not", 1, pp.opAssoc.RIGHT, ),
|
||||
("and", 2, pp.opAssoc.LEFT, ),
|
||||
("∧", 2, pp.opAssoc.LEFT, ),
|
||||
("or", 2, pp.opAssoc.LEFT, ),
|
||||
("∨", 2, pp.opAssoc.LEFT, ),
|
||||
])
|
||||
|
||||
|
||||
def _parsed_query2dict(parsed_query):
|
||||
result = None
|
||||
while parsed_query:
|
||||
part = parsed_query.pop()
|
||||
if part in binary_operator:
|
||||
result = {part: {parsed_query.pop(): result}}
|
||||
|
||||
elif part in multiple_operators:
|
||||
if result.get(part):
|
||||
result[part].append(
|
||||
_parsed_query2dict(parsed_query.pop()))
|
||||
else:
|
||||
result = {part: [result]}
|
||||
|
||||
elif part in uninary_operators:
|
||||
result = {part: result}
|
||||
elif isinstance(part, pp.ParseResults):
|
||||
kind = part.getName()
|
||||
if kind == "list":
|
||||
res = part.asList()
|
||||
else:
|
||||
res = _parsed_query2dict(part)
|
||||
if result is None:
|
||||
result = res
|
||||
elif isinstance(result, dict):
|
||||
result.values()[0].append(res)
|
||||
else:
|
||||
result = part
|
||||
return result
|
||||
|
||||
|
||||
def search_query_builder(query):
|
||||
parsed_query = expr.parseString(query)[0]
|
||||
return _parsed_query2dict(parsed_query)
|
||||
@@ -20,6 +20,7 @@ from cliff import lister
|
||||
from cliff import show
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from gnocchiclient import utils
|
||||
from gnocchiclient.v1 import base
|
||||
|
||||
|
||||
@@ -52,6 +53,17 @@ class ResourceManager(base.Manager):
|
||||
url = self.client.url("resource/generic/%s" % (resource_id))
|
||||
self.client.api.delete(url)
|
||||
|
||||
def search(self, resource_type="generic", details=False, history=False,
|
||||
request=None):
|
||||
request = request or {}
|
||||
details = "true" if details else "false"
|
||||
history = "true" if history else "false"
|
||||
url = self.client.url("/search/resource/%s?details=%s&history=%s" % (
|
||||
resource_type, details, history))
|
||||
return self.client.api.post(
|
||||
url, headers={'Content-Type': "application/json"},
|
||||
data=jsonutils.dumps(request)).json()
|
||||
|
||||
|
||||
class CliResourceList(lister.Lister):
|
||||
COLS = ('id', 'type',
|
||||
@@ -83,6 +95,22 @@ class CliResourceList(lister.Lister):
|
||||
return tuple([resource[k] for k in cls.COLS])
|
||||
|
||||
|
||||
class CliResourceSearch(CliResourceList):
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CliResourceSearch, self).get_parser(prog_name)
|
||||
parser.add_argument("-q", "--query",
|
||||
help="Query"),
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
resources = self.app.client.resource.search(
|
||||
resource_type=parsed_args.resource_type,
|
||||
details=parsed_args.details,
|
||||
history=parsed_args.history,
|
||||
request=utils.search_query_builder(parsed_args.query))
|
||||
return self.COLS, [self._resource2tuple(r) for r in resources]
|
||||
|
||||
|
||||
def normalize_metrics(res):
|
||||
res['metrics'] = "\n".join(sorted(
|
||||
["%s: %s" % (name, _id)
|
||||
@@ -131,7 +159,7 @@ class CliResourceCreate(show.ShowOne):
|
||||
attr, __, value = attr.partition(":")
|
||||
resource[attr] = value
|
||||
if parsed_args.metric:
|
||||
rid = getattr(parsed_args, 'resource_id')
|
||||
rid = getattr(parsed_args, 'resource_id', None)
|
||||
if rid:
|
||||
r = self.app.client.resource.get(parsed_args.resource_type,
|
||||
parsed_args.resource_id)
|
||||
|
||||
@@ -30,6 +30,7 @@ console_scripts =
|
||||
gnocchi.cli.v1 =
|
||||
resource_list = gnocchiclient.v1.resource:CliResourceList
|
||||
resource_show = gnocchiclient.v1.resource:CliResourceShow
|
||||
resource_search = gnocchiclient.v1.resource:CliResourceSearch
|
||||
resource_create = gnocchiclient.v1.resource:CliResourceCreate
|
||||
resource_update = gnocchiclient.v1.resource:CliResourceUpdate
|
||||
resource_delete = gnocchiclient.v1.resource:CliResourceDelete
|
||||
|
||||
Reference in New Issue
Block a user