From 6ce311d0009baa787d745acad37c2749071a94b1 Mon Sep 17 00:00:00 2001 From: Ildiko Vancsa Date: Tue, 14 Jan 2014 19:26:14 +0100 Subject: [PATCH] Implements in operator for complex query functionality Implements: blueprint complex-filter-expressions-in-api-queries Change-Id: I6ee7249f605f2afdeae59adf97f588cf1075a453 --- ceilometer/api/controllers/v2.py | 35 +++++++++++++++++-- ceilometer/storage/impl_mongodb.py | 3 +- ceilometer/storage/impl_sqlalchemy.py | 7 +++- ceilometer/tests/api/v2/test_complex_query.py | 8 +++++ .../api/v2/test_complex_query_scenarios.py | 12 +++++++ .../tests/storage/test_storage_scenarios.py | 28 +++++++++++++++ doc/source/webapi/v2.rst | 2 +- 7 files changed, 90 insertions(+), 5 deletions(-) diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index 14d3df68b..00bc08b73 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -1015,6 +1015,11 @@ class ValidatedComplexQuery(object): "minProperties": 1, "maxProperties": 1} + schema_value_in = { + "type": "array", + "items": {"oneOf": [{"type": "string"}, + {"type": "number"}]}} + schema_field = { "type": "object", "patternProperties": {"[\S]+": schema_value}, @@ -1022,6 +1027,13 @@ class ValidatedComplexQuery(object): "minProperties": 1, "maxProperties": 1} + schema_field_in = { + "type": "object", + "patternProperties": {"[\S]+": schema_value_in}, + "additionalProperties": False, + "minProperties": 1, + "maxProperties": 1} + schema_leaf = { "type": "object", "patternProperties": {simple_ops: schema_field}, @@ -1029,6 +1041,20 @@ class ValidatedComplexQuery(object): "minProperties": 1, "maxProperties": 1} + schema_leaf_in = { + "type": "object", + "patternProperties": {"(?i)^in$": schema_field_in}, + "additionalProperties": False, + "minProperties": 1, + "maxProperties": 1} + + schema_leaf_simple_ops = { + "type": "object", + "patternProperties": {simple_ops: schema_field}, + "additionalProperties": False, + "minProperties": 1, + "maxProperties": 1} + schema_and_or_array = { "type": "array", "items": {"$ref": "#"}, @@ -1042,11 +1068,13 @@ class ValidatedComplexQuery(object): "maxProperties": 1} schema = { - "oneOf": [{"$ref": "#/definitions/leaf"}, + "oneOf": [{"$ref": "#/definitions/leaf_simple_ops"}, + {"$ref": "#/definitions/leaf_in"}, {"$ref": "#/definitions/and_or"}], "minProperties": 1, "maxProperties": 1, - "definitions": {"leaf": schema_leaf, + "definitions": {"leaf_simple_ops": schema_leaf_simple_ops, + "leaf_in": schema_leaf_in, "and_or": schema_and_or}} orderby_schema = { @@ -1074,6 +1102,9 @@ class ValidatedComplexQuery(object): self.schema_field["patternProperties"] = { valid_fields: self.schema_value} + self.schema_field_in["patternProperties"] = { + valid_fields: self.schema_value_in} + self.orderby_schema["items"]["patternProperties"] = { valid_fields: {"type": "string", "pattern": self.order_directions}} diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index 62fff58b4..fbf2b1df2 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -344,7 +344,8 @@ class Connection(base.Connection): "=<": "$lte", ">=": "$gte", "=>": "$gte", - "!=": "$ne"} + "!=": "$ne", + "in": "$in"} complex_operators = {"or": "$or", "and": "$and"} diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index 91a4baa64..2d65b26d9 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -169,6 +169,10 @@ def make_query_from_filter(session, query, sample_filter, require_meter=True): return query +def operator_in(field_name, field_value): + return field_name.in_(field_value) + + class Connection(base.Connection): """SqlAlchemy connection.""" @@ -179,7 +183,8 @@ class Connection(base.Connection): "=<": operator.le, ">=": operator.ge, "=>": operator.ge, - "!=": operator.ne} + "!=": operator.ne, + "in": operator_in} complex_operators = {"or": or_, "and": and_} ordering_functions = {"asc": asc, diff --git a/ceilometer/tests/api/v2/test_complex_query.py b/ceilometer/tests/api/v2/test_complex_query.py index 4c72c6f69..70d69d67c 100644 --- a/ceilometer/tests/api/v2/test_complex_query.py +++ b/ceilometer/tests/api/v2/test_complex_query.py @@ -300,6 +300,14 @@ class TestFilterSyntaxValidation(test.BaseTestCase): {"=": {"counter_name": "value"}}]} self.query._validate_filter(filter) + def test_complex_operator_with_in(self): + filter = {"and": [{"<": {"counter_volume": 42}}, + {">=": {"counter_volume": 36}}, + {"in": {"project_id": ["project_id1", + "project_id2", + "project_id3"]}}]} + self.query._validate_filter(filter) + def test_invalid_complex_operator(self): filter = {"xor": [{"=": {"project_id": "string_value"}}, {"=": {"resource_id": "value"}}]} diff --git a/ceilometer/tests/api/v2/test_complex_query_scenarios.py b/ceilometer/tests/api/v2/test_complex_query_scenarios.py index 8f9aff293..16f2d2d3c 100644 --- a/ceilometer/tests/api/v2/test_complex_query_scenarios.py +++ b/ceilometer/tests/api/v2/test_complex_query_scenarios.py @@ -153,6 +153,18 @@ class TestQueryMetersController(tests_api.FunctionalTest, self.assertIn(sample['project_id'], (["project-id1", "project-id2"])) + def test_admin_tenant_sees_every_project_with_in_filter(self): + filter = ('{"In": ' + + '{"project_id": ["project-id1", "project-id2"]}}') + data = self.post_json(self.url, + params={"filter": filter}, + headers=admin_header) + + self.assertEqual(2, len(data.json)) + for sample in data.json: + self.assertIn(sample['project_id'], + (["project-id1", "project-id2"])) + def test_admin_tenant_can_query_any_project(self): data = self.post_json(self.url, params={"filter": diff --git a/ceilometer/tests/storage/test_storage_scenarios.py b/ceilometer/tests/storage/test_storage_scenarios.py index ec3492128..b4989e0a9 100644 --- a/ceilometer/tests/storage/test_storage_scenarios.py +++ b/ceilometer/tests/storage/test_storage_scenarios.py @@ -731,6 +731,14 @@ class ComplexSampleQueryTest(DBTestBase, {"and": [{"=": {"counter_name": "cpu_util"}}, {"and": and_expression}]}]} + in_expression = {"in": {"resource_id": ["resource-id-42", + "resource-id-43", + "resource-id-44"]}} + self.complex_filter_in = {"and": + [in_expression, + {"and": + [{"=": {"counter_name": "cpu_util"}}, + {"and": and_expression}]}]} def _create_samples(self): for resource in range(42, 45): @@ -882,6 +890,26 @@ class ComplexSampleQueryTest(DBTestBase, orderby=orderby)) self.assertRaises(KeyError, query) + def test_query_complex_filter_with_in(self): + self._create_samples() + results = list( + self.conn.query_samples(filter_expr=self.complex_filter_in)) + self.assertEqual(len(results), 9) + for sample in results: + self.assertIn(sample.resource_id, + set(["resource-id-42", + "resource-id-43", + "resource-id-44"])) + self.assertEqual(sample.counter_name, + "cpu_util") + self.assertTrue(sample.counter_volume > 0.4) + self.assertTrue(sample.counter_volume <= 0.8) + + def test_query_filter_with_empty_in(self): + results = list( + self.conn.query_samples(filter_expr={"in": {"resource_id": []}})) + self.assertEqual(len(results), 0) + class StatisticsTest(DBTestBase, tests_db.MixinTestsWithBackendScenarios): diff --git a/doc/source/webapi/v2.rst b/doc/source/webapi/v2.rst index ca94d9cae..fc1f3b1a7 100644 --- a/doc/source/webapi/v2.rst +++ b/doc/source/webapi/v2.rst @@ -95,7 +95,7 @@ Complex Query +++++++++++++ The filter expressions of the Complex Query feature operate on the fields of *Sample*, *Alarm* and *AlarmChange*. The following comparison operators are -supported: *=*, *!=*, *<*, *<=*, *>* and *>=*; and the following logical +supported: *=*, *!=*, *<*, *<=*, *>*, *>=* and *in*; and the following logical operators can be used: *and* and *or*. The field names are validated against the database models.