diff --git a/muranoapi/api/v1/__init__.py b/muranoapi/api/v1/__init__.py index f95052cd..0a985c22 100644 --- a/muranoapi/api/v1/__init__.py +++ b/muranoapi/api/v1/__init__.py @@ -17,6 +17,15 @@ from muranoapi.db import session as db_session stats = None +SUPPORTED_PARAMS = ('order_by', 'category', 'marker', 'tag', 'class', + 'limit', 'type', 'fqn', 'category', 'owned') +LIST_PARAMS = ('category', 'tag', 'class', 'order_by') +ORDER_VALUES = ('fqn', 'name', 'created') +SEARCH_MAPPING = {'fqn': 'fully_qualified_name', + 'name': 'name', + 'created': 'created' + } + def get_draft(environment_id=None, session_id=None): unit = db_session.get_session() diff --git a/muranoapi/api/v1/catalog.py b/muranoapi/api/v1/catalog.py index ca43a69a..50f6f516 100644 --- a/muranoapi/api/v1/catalog.py +++ b/muranoapi/api/v1/catalog.py @@ -16,6 +16,7 @@ from oslo.config import cfg from webob import exc +import muranoapi.api.v1 from muranoapi.db.catalog import api as db_api from muranoapi.openstack.common import exception from muranoapi.openstack.common.gettextutils import _ # noqa @@ -26,6 +27,10 @@ from muranoapi.openstack.common import wsgi LOG = logging.getLogger(__name__) CONF = cfg.CONF +SUPPORTED_PARAMS = muranoapi.api.v1.SUPPORTED_PARAMS +LIST_PARAMS = muranoapi.api.v1.LIST_PARAMS +ORDER_VALUES = muranoapi.api.v1.ORDER_VALUES + def _check_content_type(req, content_type): try: @@ -36,6 +41,30 @@ def _check_content_type(req, content_type): raise exc.HTTPBadRequest(explanation=msg) +def _get_filters(query_params): + filters = {} + for param_pair in query_params: + k, v = param_pair + if k not in SUPPORTED_PARAMS: + LOG.warning(_("Search by parameter '{name}' " + "is not supported. Skipping it.").format(name=k)) + continue + + if k in LIST_PARAMS: + filters.setdefault(k, []).append(v) + else: + filters[k] = v + order_by = filters.get('order_by', []) + for i in order_by[:]: + if ORDER_VALUES and i not in ORDER_VALUES: + filters['order_by'].remove(i) + LOG.warning(_("Value of 'order_by' parameter is not valid. " + "Allowed values are: {0}. Skipping it.").format( + ", ".join(ORDER_VALUES))) + + return filters + + class Controller(object): """ WSGI controller for application catalog resource in Murano v1 API @@ -66,6 +95,11 @@ class Controller(object): package = db_api.package_get(package_id, req.context) return package.to_dict() + def search(self, req): + filters = _get_filters(req.GET._items) + packages = db_api.package_search(filters, req.context) + return {"packages": [package.to_dict() for package in packages]} + def create_resource(): return wsgi.Resource(Controller()) diff --git a/muranoapi/api/v1/router.py b/muranoapi/api/v1/router.py index f1c99693..c0f0e7cd 100644 --- a/muranoapi/api/v1/router.py +++ b/muranoapi/api/v1/router.py @@ -129,4 +129,8 @@ class API(wsgi.Router): controller=catalog_resource, action='update', conditions={'method': ['PATCH']}) + mapper.connect('/catalog/packages', + controller=catalog_resource, + action='search', + conditions={'method': ['GET']}) super(API, self).__init__(mapper) diff --git a/muranoapi/db/catalog/api.py b/muranoapi/db/catalog/api.py index 87a6ca8c..4c89e31f 100644 --- a/muranoapi/db/catalog/api.py +++ b/muranoapi/db/catalog/api.py @@ -12,13 +12,17 @@ # License for the specific language governing permissions and limitations # under the License. +from sqlalchemy import or_ +from sqlalchemy import sql from webob import exc +import muranoapi from muranoapi.db import models from muranoapi.db import session as db_session from muranoapi.openstack.common.gettextutils import _ # noqa from muranoapi.openstack.common import log as logging +SEARCH_MAPPING = muranoapi.api.v1.SEARCH_MAPPING LOG = logging.getLogger(__name__) @@ -204,3 +208,104 @@ def package_update(pkg_id, changes, context): with session.begin(): session.add(pkg) return pkg + + +def package_search(filters, context): + """ + Search packages with different filters + * Admin is allowed to browse all the packages + * Regular user is allowed to browse all packages belongs to user tenant + and all other packages marked is_public. + Also all packages should be enabled. + * Use marker and limit for pagination: + The typical pattern of limit and marker is to make an initial limited + request and then to use the ID of the last package from the response + as the marker parameter in a subsequent limited request. + """ + + def _validate_limit(value): + if value is None: + return + try: + value = int(value) + except ValueError: + msg = _("limit param must be an integer") + raise exc.HTTPBadRequest(explanation=msg) + + if value < 0: + msg = _("limit param must be positive") + raise exc.HTTPBadRequest(explanation=msg) + + return value + + def get_pkg_attr(package_obj, search_attr_name): + return getattr(package_obj, SEARCH_MAPPING[search_attr_name]) + + limit = _validate_limit(filters.get('limit')) + + session = db_session.get_session() + pkg = models.Package + + if 'owned' in filters.keys(): + query = session.query(pkg).filter(pkg.owner_id == context.tenant) + elif context.is_admin: + query = session.query(pkg) + else: + query = session.query(pkg).filter(or_((pkg.is_public & pkg.enabled), + pkg.owner_id == context.tenant)) + + if 'category' in filters.keys(): + query = query.filter(pkg.categories.any( + models.Category.name.in_(filters['category']))) + if 'tag' in filters.keys(): + query = query.filter(pkg.tags.any( + models.Tag.name.in_(filters['tag']))) + if 'class' in filters.keys(): + query = query.filter(pkg.class_definition.any( + models.Class.name.in_(filters['class']))) + if 'fqn' in filters.keys(): + query = query.filter(pkg.fully_qualified_name == filters['fqn']) + + sort_keys = filters.get('order_by', []) or ['created'] + for key in sort_keys: + query = query.order_by(get_pkg_attr(pkg, key)) + + marker = filters.get('marker') + if marker is not None: + # Note(efedorova): Copied from Glance + # Pagination works by requiring a unique sort_key, specified by sort_ + # keys. (If sort_keys is not unique, then we risk looping through + # values.) We use the last row in the previous page as the 'marker' + # for pagination. So we must return values that follow the passed + # marker in the order. With a single-valued sort_key, this would + # be easy: sort_key > X. With a compound-values sort_key, + # (k1, k2, k3) we must do this to repeat the lexicographical + # ordering: (k1 > X1) or (k1 == X1 && k2 > X2) or + # (k1 == X1 && k2 == X2 && k3 > X3) + + model_marker = _package_get(marker, session) + marker_values = [] + for sort_key in sort_keys: + v = getattr(model_marker, sort_key) + marker_values.append(v) + + # Build up an array of sort criteria as in the docstring + criteria_list = [] + for i in range(len(sort_keys)): + crit_attrs = [] + for j in range(i): + model_attr = get_pkg_attr(pkg, sort_keys[j]) + crit_attrs.append((model_attr == marker_values[j])) + + model_attr = get_pkg_attr(pkg, sort_keys[i]) + crit_attrs.append((model_attr > marker_values[i])) + + criteria = sql.and_(*crit_attrs) + criteria_list.append(criteria) + f = sql.or_(*criteria_list) + query = query.filter(f) + + if limit is not None: + query = query.limit(limit) + + return query.all()