# aodh.api.controllers.v2.query

# Source code for aodh.api.controllers.v2.query

    #
    # Copyright 2012 New Dream Network, LLC (DreamHost)
    # Copyright 2013 IBM Corp.
    # Copyright 2013 eNovance <licensing@enovance.com>
    # Copyright Ericsson AB 2013. All rights reserved
    # Copyright 2014 Hewlett-Packard Company
    # Copyright 2015 Huawei Technologies Co., Ltd.
    #
    # 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
    import json

    import jsonschema
    from oslo_log import log
    from oslo_utils import timeutils
    import pecan
    from pecan import rest
    from wsme import types as wtypes
    import wsmeext.pecan as wsme_pecan

    from aodh.api.controllers.v2 import alarms
    from aodh.api.controllers.v2 import base
    from aodh.api import rbac
    from aodh.i18n import _
    from aodh import profiler
    from aodh.storage import models

    LOG = log.getLogger(__name__)

    class ComplexQuery(base.Base):
        """Holds a sample query encoded in json."""

        filter = wtypes.text
        "The filter expression encoded in json."

        orderby = wtypes.text
        "List of single-element dicts for specifing the ordering of the results."

        limit = int
        "The maximum number of results to be returned."

        @classmethod
        def sample(cls):
            return cls(filter='{"and": [{"and": [{"=": ' +
                              '{"counter_name": "cpu_util"}}, ' +
                              '{">": {"counter_volume": 0.23}}, ' +
                              '{"<": {"counter_volume": 0.26}}]}, ' +
                              '{"or": [{"and": [{">": ' +
                              '{"timestamp": "2013-12-01T18:00:00"}}, ' +
                              '{"<": ' +
                              '{"timestamp": "2013-12-01T18:15:00"}}]}, ' +
                              '{"and": [{">": ' +
                              '{"timestamp": "2013-12-01T18:30:00"}}, ' +
                              '{"<": ' +
                              '{"timestamp": "2013-12-01T18:45:00"}}]}]}]}',
                       orderby='[{"counter_volume": "ASC"}, ' +
                               '{"timestamp": "DESC"}]',
                       limit=42
                       )

    def _list_to_regexp(items, regexp_prefix=""):
        regexp = ["^%s$" % item for item in items]
        regexp = regexp_prefix + "|".join(regexp)
        return regexp

    @profiler.trace_cls('api')
    class ValidatedComplexQuery:
        complex_operators = ["and", "or"]
        order_directions = ["asc", "desc"]
        simple_ops = ["=", "!=", "<", ">", "<=", "=<", ">=", "=>", "=~"]
        regexp_prefix = "(?i)"

        complex_ops = _list_to_regexp(complex_operators, regexp_prefix)
        simple_ops = _list_to_regexp(simple_ops, regexp_prefix)
        order_directions = _list_to_regexp(order_directions, regexp_prefix)

        timestamp_fields = ["timestamp", "state_timestamp"]

        def __init__(self, query, db_model, additional_name_mapping=None,
                     metadata_allowed=False):
            additional_name_mapping = additional_name_mapping or {}
            self.name_mapping = {"user": "user_id",
                                 "project": "project_id"}
            self.name_mapping.update(additional_name_mapping)
            valid_keys = db_model.get_field_names()
            valid_keys = list(valid_keys) + list(self.name_mapping.keys())
            valid_fields = _list_to_regexp(valid_keys)

            if metadata_allowed:
                valid_filter_fields = valid_fields + r"|^metadata\.[\S]+$"
            else:
                valid_filter_fields = valid_fields

            schema_value = {
                "oneOf": [{"type": "string"},
                          {"type": "number"},
                          {"type": "boolean"}],
                "minProperties": 1,
                "maxProperties": 1}

            schema_value_in = {
                "type": "array",
                "items": {"oneOf": [{"type": "string"},
                                    {"type": "number"}]},
                "minItems": 1}

            schema_field = {
                "type": "object",
                "patternProperties": {valid_filter_fields: schema_value},
                "additionalProperties": False,
                "minProperties": 1,
                "maxProperties": 1}

            schema_field_in = {
                "type": "object",
                "patternProperties": {valid_filter_fields: schema_value_in},
                "additionalProperties": False,
                "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": {self.simple_ops: schema_field},
                "additionalProperties": False,
                "minProperties": 1,
                "maxProperties": 1}

            schema_and_or_array = {
                "type": "array",
                "items": {"$ref": "#"},
                "minItems": 2}

            schema_and_or = {
                "type": "object",
                "patternProperties": {self.complex_ops: schema_and_or_array},
                "additionalProperties": False,
                "minProperties": 1,
                "maxProperties": 1}

            schema_not = {
                "type": "object",
                "patternProperties": {"(?i)^not$": {"$ref": "#"}},
                "additionalProperties": False,
                "minProperties": 1,
                "maxProperties": 1}

            self.schema = {
                "oneOf": [{"$ref": "#/definitions/leaf_simple_ops"},
                          {"$ref": "#/definitions/leaf_in"},
                          {"$ref": "#/definitions/and_or"},
                          {"$ref": "#/definitions/not"}],
                "minProperties": 1,
                "maxProperties": 1,
                "definitions": {"leaf_simple_ops": schema_leaf_simple_ops,
                                "leaf_in": schema_leaf_in,
                                "and_or": schema_and_or,
                                "not": schema_not}}

            self.orderby_schema = {
                "type": "array",
                "items": {
                    "type": "object",
                    "patternProperties":
                        {valid_fields:
                            {"type": "string",
                             "pattern": self.order_directions}},
                    "additionalProperties": False,
                    "minProperties": 1,
                    "maxProperties": 1}}

            self.original_query = query

        def validate(self, visibility_field):
            """Validates the query content and does the necessary conversions."""
            if self.original_query.filter is wtypes.Unset:
                self.filter_expr = None
            else:
                try:
                    self.filter_expr = json.loads(self.original_query.filter)
                    self._validate_filter(self.filter_expr)
                except (ValueError, jsonschema.exceptions.ValidationError) as e:
                    raise base.ClientSideError(
                        _("Filter expression not valid: %s") % str(e))
                self._replace_isotime_with_datetime(self.filter_expr)
                self._convert_operator_to_lower_case(self.filter_expr)
                self._normalize_field_names_for_db_model(self.filter_expr)

            self._force_visibility(visibility_field)

            if self.original_query.orderby is wtypes.Unset:
                self.orderby = None
            else:
                try:
                    self.orderby = json.loads(self.original_query.orderby)
                    self._validate_orderby(self.orderby)
                except (ValueError, jsonschema.exceptions.ValidationError) as e:
                    raise base.ClientSideError(
                        _("Order-by expression not valid: %s") % e)
                self._convert_orderby_to_lower_case(self.orderby)
                self._normalize_field_names_in_orderby(self.orderby)

            if self.original_query.limit is wtypes.Unset:
                self.limit = None
            else:
                self.limit = self.original_query.limit

            if self.limit is not None and self.limit <= 0:
                msg = _('Limit should be positive')
                raise base.ClientSideError(msg)

        @staticmethod
        def lowercase_values(mapping):
            """Converts the values in the mapping dict to lowercase."""
            items = mapping.items()
            for key, value in items:
                mapping[key] = value.lower()

        def _convert_orderby_to_lower_case(self, orderby):
            for orderby_field in orderby:
                self.lowercase_values(orderby_field)

        def _normalize_field_names_in_orderby(self, orderby):
            for orderby_field in orderby:
                self._replace_field_names(orderby_field)

        def _traverse_postorder(self, tree, visitor):
            op = list(tree.keys())[0]
            if op.lower() in self.complex_operators:
                for i, operand in enumerate(tree[op]):
                    self._traverse_postorder(operand, visitor)
            if op.lower() == "not":
                self._traverse_postorder(tree[op], visitor)

            visitor(tree)

        def _check_cross_project_references(self, own_project_id,
                                            visibility_field):
            """Do not allow other than own_project_id."""
            def check_project_id(subfilter):
                op, value = list(subfilter.items())[0]
                if (op.lower() not in self.complex_operators
                        and list(value.keys())[0] == visibility_field
                        and value[visibility_field] != own_project_id):
                    raise base.ProjectNotAuthorized(value[visibility_field])

            self._traverse_postorder(self.filter_expr, check_project_id)

        def _force_visibility(self, visibility_field):
            """Force visibility field.

            If the tenant is not admin insert an extra
            "and <visibility_field>=<tenant's project_id>" clause to the query.
            """
            authorized_project = rbac.get_limited_to_project(
                pecan.request, pecan.request.enforcer)
            is_admin = authorized_project is None
            if not is_admin:
                self._restrict_to_project(authorized_project, visibility_field)
                self._check_cross_project_references(authorized_project,
                                                     visibility_field)

        def _restrict_to_project(self, project_id, visibility_field):
            restriction = {"=": {visibility_field: project_id}}
            if self.filter_expr is None:
                self.filter_expr = restriction
            else:
                self.filter_expr = {"and": [restriction, self.filter_expr]}

        def _replace_isotime_with_datetime(self, filter_expr):
            def replace_isotime(subfilter):
                op, value = list(subfilter.items())[0]
                if op.lower() not in self.complex_operators:
                    field = list(value.keys())[0]
                    if field in self.timestamp_fields:
                        date_time = self._convert_to_datetime(subfilter[op][field])
                        subfilter[op][field] = date_time

            self._traverse_postorder(filter_expr, replace_isotime)

        def _normalize_field_names_for_db_model(self, filter_expr):
            def _normalize_field_names(subfilter):
                op, value = list(subfilter.items())[0]
                if op.lower() not in self.complex_operators:
                    self._replace_field_names(value)
            self._traverse_postorder(filter_expr,
                                     _normalize_field_names)

        def _replace_field_names(self, subfilter):
            field, value = list(subfilter.items())[0]
            if field in self.name_mapping:
                del subfilter[field]
                subfilter[self.name_mapping[field]] = value
            if field.startswith("metadata."):
                del subfilter[field]
                subfilter["resource_" + field] = value

        @staticmethod
        def lowercase_keys(mapping):
            """Converts the values of the keys in mapping to lowercase."""
            loop_mapping = copy.deepcopy(mapping)
            items = loop_mapping.items()
            for key, value in items:
                del mapping[key]
                mapping[key.lower()] = value

        def _convert_operator_to_lower_case(self, filter_expr):
            self._traverse_postorder(filter_expr, self.lowercase_keys)

        @staticmethod
        def _convert_to_datetime(isotime):
            try:
                date_time = timeutils.parse_isotime(isotime)
                date_time = date_time.replace(tzinfo=None)
                return date_time
            except ValueError:
                LOG.exception("String %s is not a valid isotime", isotime)
                msg = _('Failed to parse the timestamp value %s') % isotime
                raise base.ClientSideError(msg)

        def _validate_filter(self, filter_expr):
            jsonschema.validate(filter_expr, self.schema)

        def _validate_orderby(self, orderby_expr):
            jsonschema.validate(orderby_expr, self.orderby_schema)

    class QueryAlarmHistoryController(rest.RestController):
        """Provides complex query possibilities for alarm history."""
        @wsme_pecan.wsexpose([alarms.AlarmChange], body=ComplexQuery)
        def post(self, body):
            """Define query for retrieving AlarmChange data.

            :param body: Query rules for the alarm history to be returned.
            """
            target = rbac.target_from_segregation_rule(
                pecan.request, pecan.request.enforcer)
            rbac.enforce('query_alarm_history', pecan.request,
                         pecan.request.enforcer, target)

            query = ValidatedComplexQuery(body,
                                          models.AlarmChange)
            query.validate(visibility_field="on_behalf_of")
            conn = pecan.request.storage
            return [alarms.AlarmChange.from_db_model(s)
                    for s in conn.query_alarm_history(query.filter_expr,
                                                      query.orderby,
                                                      query.limit)]

    class QueryAlarmsController(rest.RestController):
        """Provides complex query possibilities for alarms."""
        history = QueryAlarmHistoryController()

        @wsme_pecan.wsexpose([alarms.Alarm], body=ComplexQuery)
        def post(self, body):
            """Define query for retrieving Alarm data.

            :param body: Query rules for the alarms to be returned.
            """

            target = rbac.target_from_segregation_rule(
                pecan.request, pecan.request.enforcer)
            rbac.enforce('query_alarm', pecan.request,
                         pecan.request.enforcer, target)

            query = ValidatedComplexQuery(body,
                                          models.Alarm)
            query.validate(visibility_field="project_id")
            conn = pecan.request.storage
            return [alarms.Alarm.from_db_model(s)
                    for s in conn.query_alarms(query.filter_expr,
                                               query.orderby,
                                               query.limit)]

    class QueryController(rest.RestController):

        alarms = QueryAlarmsController()

this page last updated: 2026-08-15 16:35:58

Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.

found an error? report a bug