# 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. """ NOTE: This module is a temporary shim until networking projects move to versioned objects at which point this module shouldn't be needed. """ from oslo_db.sqlalchemy import utils as sa_utils from sqlalchemy import sql, or_, and_ from sqlalchemy.ext import associationproxy from neutron_lib._i18n import _ from neutron_lib.api import attributes from neutron_lib.db import utils as db_utils from neutron_lib import exceptions as n_exc from neutron_lib.objects import utils as obj_utils from neutron_lib.utils import helpers # Classes implementing extensions will register hooks into this dictionary # for "augmenting" the "core way" of building a query for retrieving objects # from a model class. Hooks are registered by invoking register_hook(). _model_query_hooks = { # model1 : { # hook1: { # 'query': query_hook, # 'filter': filter_hook, # 'result_filters': result_filters # }, # hook2: { # 'query': query_hook, # 'filter': filter_hook, # 'result_filters': result_filters # }, # ... # }, # model2 : { # hook1: { # 'query': query_hook, # 'filter': filter_hook, # 'result_filters': result_filters # }, # hook2: { # 'query': query_hook, # 'filter': filter_hook, # 'result_filters': result_filters # }, # ... # }, # ... } def register_hook(model, name, query_hook, filter_hook, result_filters=None): """Register a hook to be invoked when a query is executed. Adds the hook components to the _model_query_hooks dict. Models are the keys of this dict, whereas the value is another dict mapping hook names to callables performing the hook. :param model: The DB Model that the hook applies to. :param name: A name for the hook. :param query_hook: The method to be called to augment the query. :param filter_hook: A method to be called to augment the query filter. :param result_filters: A Method to be called to filter the query result. :returns: None. """ if callable(query_hook): query_hook = helpers.make_weak_ref(query_hook) if callable(filter_hook): filter_hook = helpers.make_weak_ref(filter_hook) if callable(result_filters): result_filters = helpers.make_weak_ref(result_filters) _model_query_hooks.setdefault(model, {})[name] = { 'query': query_hook, 'filter': filter_hook, 'result_filters': result_filters } def get_hooks(model): """Retrieve the model query hooks for a model. :param model: The DB Model to look up for query hooks. :returns: list of hooks """ return _model_query_hooks.get(model, {}).values() def query_with_hooks(context, model, field=None): """Query with hooks using the said context and model. :param context: The context to use for the DB session. :param model: The model to query. :param field: The column. :returns: The query with hooks applied to it. """ if field: if hasattr(model, field): field = getattr(model, field) else: msg = _("'%s' is not supported as field") % field raise n_exc.InvalidInput(error_message=msg) query = context.session.query(field) else: query = context.session.query(model) # define basic filter condition for model query query_filter = None if db_utils.model_query_scope_is_project(context, model): if hasattr(model, 'rbac_entries'): query = query.outerjoin(model.rbac_entries) rbac_model = model.rbac_entries.property.mapper.class_ query_filter = ( (model.tenant_id == context.tenant_id) | ((rbac_model.action == 'access_as_shared') & ((rbac_model.target_tenant == context.tenant_id) | (rbac_model.target_tenant == '*')))) elif hasattr(model, 'shared'): query_filter = ((model.tenant_id == context.tenant_id) | (model.shared == sql.true())) else: query_filter = (model.tenant_id == context.tenant_id) # Execute query hooks registered from mixins and plugins for hook in get_hooks(model): query_hook = helpers.resolve_ref(hook.get('query')) if query_hook: query = query_hook(context, model, query) filter_hook = helpers.resolve_ref(hook.get('filter')) if filter_hook: query_filter = filter_hook(context, model, query_filter) # NOTE(salvatore-orlando): 'if query_filter' will try to evaluate the # condition, raising an exception if query_filter is not None: query = query.filter(query_filter) return query def get_by_id(context, model, object_id): """Query the said model with the given context for a specific object. :param context: The context to use in the query. :param model: The model to query. :param object_id: The ID of the object to query for. :returns: The object with the give object_id for the said model. """ query = query_with_hooks(context=context, model=model) return query.filter(model.id == object_id).one() def apply_filters(query, model, filters, context=None): """Apply filters to a query. :param query: The query to apply filters to. :param model: The model for the query. :param filters: The filters to apply. :param context: The context to use for the DB session. :returns: The query with filters applied to it. """ if filters: for key, value in filters.items(): column = getattr(model, key, None) # NOTE(kevinbenton): if column is a hybrid property that # references another expression, attempting to convert to # a boolean will fail so we must compare to None. # See "An Important Expression Language Gotcha" in: # docs.sqlalchemy.org/en/rel_0_9/changelog/migration_06.html if column is not None: if not value: query = query.filter(sql.false()) return query if isinstance(column, associationproxy.AssociationProxy): # association proxies don't support in_ so we have to # do multiple equals matches query = query.filter( or_(*[column == v for v in value])) elif isinstance(value, obj_utils.FilterObj): query = query.filter(value.filter(column)) elif None in value: # in_() operator does not support NULL element so we have # to do multiple equals matches query = query.filter( or_(*[column == v for v in value])) else: query = query.filter(column.in_(value)) elif key == 'shared' and hasattr(model, 'rbac_entries'): # translate a filter on shared into a query against the # object's rbac entries rbac = model.rbac_entries.property.mapper.class_ matches = [rbac.target_tenant == '*'] if context: matches.append(rbac.target_tenant == context.tenant_id) # any 'access_as_shared' records that match the # wildcard or requesting tenant is_shared = and_(rbac.action == 'access_as_shared', or_(*matches)) if not value[0]: # NOTE(kevinbenton): we need to find objects that don't # have an entry that matches the criteria above so # we use a subquery to exclude them. # We can't just filter the inverse of the query above # because that will still give us a network shared to # our tenant (or wildcard) if it's shared to another # tenant. # This is the column joining the table to rbac via # the object_id. We can't just use model.id because # subnets join on network.id so we have to inspect the # relationship. join_cols = model.rbac_entries.property.local_columns oid_col = list(join_cols)[0] is_shared = ~oid_col.in_( query.session.query(rbac.object_id).filter(is_shared) ) elif (not context or not db_utils.model_query_scope_is_project( context, model)): # we only want to join if we aren't using the subquery # and if we aren't already joined because this is a # scoped query query = query.outerjoin(model.rbac_entries) query = query.filter(is_shared) for hook in get_hooks(model): result_filter = helpers.resolve_ref( hook.get('result_filters', None)) if result_filter: query = result_filter(query, filters) return query def get_collection_query(context, model, filters=None, sorts=None, limit=None, marker_obj=None, page_reverse=False): """Get a collection query. :param context: The context to use for the DB session. :param model: The model to use. :param filters: The filters to apply in the query. :param sorts: The sort keys to use. :param limit: The limit associated with the query. :param marker_obj: The marker object if applicable. :param page_reverse: If reverse paging should be used. :returns: A paginated query for the said model. """ collection = query_with_hooks(context, model) collection = apply_filters(collection, model, filters, context) if sorts: sort_keys = db_utils.get_and_validate_sort_keys(sorts, model) sort_dirs = db_utils.get_sort_dirs(sorts, page_reverse) # we always want deterministic results for sorted queries # so add unique keys to limit queries when present. # (http://docs.sqlalchemy.org/en/latest/orm/ # loading_relationships.html#subqueryload-ordering) # (http://docs.sqlalchemy.org/en/latest/faq/ # ormconfiguration.html#faq-subqueryload-limit-sort) for k in _unique_keys(model): if k not in sort_keys: sort_keys.append(k) sort_dirs.append('asc') collection = sa_utils.paginate_query(collection, model, limit, marker=marker_obj, sort_keys=sort_keys, sort_dirs=sort_dirs) return collection def _unique_keys(model): # just grab first set of unique keys and use them. # if model has no unqiue sets, 'paginate_query' will # warn if sorting is unstable uk_sets = sa_utils.get_unique_keys(model) return uk_sets[0] if uk_sets else [] def get_collection(context, model, dict_func, filters=None, fields=None, sorts=None, limit=None, marker_obj=None, page_reverse=False): """Get a collection for a said model. :param context: The context to use for the DB session. :param model: The model for the collection. :param dict_func: The function used to build the collection dict. :param filters: The filters to apply. :param fields: The fields to collect. :param sorts: The sort keys to use. :param limit: The limit for the query if applicable. :param marker_obj: The marker object if applicable. :param page_reverse: If reverse paging should be used. :returns: A list of dicts where each dict is an object in the collection. """ query = get_collection_query(context, model, filters=filters, sorts=sorts, limit=limit, marker_obj=marker_obj, page_reverse=page_reverse) items = [ attributes.populate_project_info( dict_func(c, fields) if dict_func else c) for c in query ] if limit and page_reverse: items.reverse() return items def get_values(context, model, field, filters=None): query = query_with_hooks(context, model, field=field) query = apply_filters(query, model, filters, context) return [c[0] for c in query] def get_collection_count(context, model, filters=None): """Get the count for a specific collection. :param context: The context to use for the DB session. :param model: The model for the query. :param filters: The filters to apply. :returns: The number of objects for said model with filters applied. """ return get_collection_query(context, model, filters).count()