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 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.storage import models

LOG = log.getLogger(__name__)


[docs]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."
[docs] @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
[docs]class ValidatedComplexQuery(object): 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 + "|^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
[docs] 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)
[docs] @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.headers, 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
[docs] @staticmethod def lowercase_keys(mapping): """Converts the values of the keys in mapping to lowercase.""" items = 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)
[docs]class QueryAlarmHistoryController(rest.RestController): """Provides complex query possibilities for alarm history."""
[docs] @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.headers, pecan.request.enforcer) rbac.enforce('query_alarm_history', pecan.request.headers, 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)]
[docs]class QueryAlarmsController(rest.RestController): """Provides complex query possibilities for alarms.""" history = QueryAlarmHistoryController()
[docs] @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.headers, pecan.request.enforcer) rbac.enforce('query_alarm', pecan.request.headers, 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)]
[docs]class QueryController(rest.RestController): alarms = QueryAlarmsController()
Creative Commons Attribution 3.0 License

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