diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index d52b12d71..57600bc75 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -1053,6 +1053,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}, @@ -1060,6 +1065,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}, @@ -1067,6 +1079,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": "#"}, @@ -1080,11 +1106,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 = { @@ -1112,6 +1140,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 49a10fe52..83abe40f1 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -255,7 +255,8 @@ class Connection(pymongo_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 d44da0a70..dde99b345 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -180,6 +180,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.""" @@ -190,7 +194,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 d7a99613a..566106ab6 100644 --- a/ceilometer/tests/storage/test_storage_scenarios.py +++ b/ceilometer/tests/storage/test_storage_scenarios.py @@ -747,6 +747,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): @@ -900,6 +908,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 38784b832..440c60204 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.