deb-murano/muranoapi/db/catalog/api.py

404 lines
14 KiB
Python

# Copyright (c) 2014 Mirantis, Inc.
#
# 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.
from sqlalchemy import inspection
from sqlalchemy import or_
from sqlalchemy.orm import attributes
from sqlalchemy import sql
from webob import exc
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 = {'fqn': 'fully_qualified_name',
'name': 'name',
'created': 'created'
}
LOG = logging.getLogger(__name__)
def _package_get(package_id, session):
package = session.query(models.Package).get(package_id)
if not package:
msg = _("Package id '{0}' is not found").format(package_id)
LOG.error(msg)
raise exc.HTTPNotFound(msg)
return package
def _authorize_package(package, context, allow_public=False):
if context.is_admin:
return
if package.owner_id != context.tenant:
if not allow_public:
msg = _("Package '{0}' is not owned by "
"tenant '{1}'").format(package.id, context.tenant)
LOG.error(msg)
raise exc.HTTPForbidden(msg)
if not package.is_public:
msg = _("Package '{0}' is not public and not owned by "
"tenant '{1}' ").format(package.id, context.tenant)
LOG.error(msg)
raise exc.HTTPForbidden(msg)
def package_get(package_id, context):
"""
Return package details
:param package_id: ID of a package, string
:returns: detailed information about package, dict
"""
session = db_session.get_session()
package = _package_get(package_id, session)
_authorize_package(package, context, allow_public=True)
return package
def _get_categories(category_names, session=None):
"""
Return existing category objects or raise an exception
:param category_names: name of categories to associate with package, list
:returns: list of Category objects to associate with package, list
"""
if session is None:
session = db_session.get_session()
categories = []
for ctg_name in category_names:
ctg_obj = session.query(models.Category).filter_by(
name=ctg_name).first()
if not ctg_obj:
msg = _("Category '{name}' doesn't exist").format(name=ctg_name)
LOG.error(msg)
# it's not allowed to specify non-existent categories
raise exc.HTTPBadRequest(msg)
categories.append(ctg_obj)
return categories
def _get_tags(tag_names, session=None):
"""
Return existing tags object or create new ones
:param tag_names: name of tags to associate with package, list
:returns: list of Tag objects to associate with package, list
"""
if session is None:
session = db_session.get_session()
tags = []
for tag_name in tag_names:
tag_obj = session.query(models.Tag).filter_by(name=tag_name).first()
if tag_obj:
tags.append(tag_obj)
else:
tag_record = models.Tag(name=tag_name)
tags.append(tag_record)
return tags
def _get_class_definitions(class_names, session=None):
classes = []
for name in class_names:
class_record = models.Class(name=name)
classes.append(class_record)
return classes
def _do_replace(package, change):
path = change['path'][0]
value = change['value']
calculate = {'categories': _get_categories,
'tags': _get_tags}
if path in ('categories', 'tags'):
existing_items = getattr(package, path)
duplicates = list(set(i.name for i in existing_items) & set(value))
unique_values = [x for x in value if x not in duplicates]
items_to_replace = calculate[path](unique_values)
# NOTE(efedorova): Replacing duplicate entities is not allowed,
# so need to remove anything, but duplicates
# and append everything but duplicates
for item in list(existing_items):
if item.name not in duplicates:
existing_items.remove(item)
existing_items.extend(items_to_replace)
else:
setattr(package, path, value)
return package
def _do_add(package, change):
# Only categories and tags support addition
path = change['path'][0]
value = change['value']
calculate = {'categories': _get_categories,
'tags': _get_tags}
items_to_add = calculate[path](value)
for item in items_to_add:
try:
getattr(package, path).append(item)
except AssertionError:
msg = _('One of the specified {0} is already '
'associated with a package. Doing nothing.')
LOG.warning(msg.format(path))
return package
def _do_remove(package, change):
# Only categories and tags support removal
def find(seq, predicate):
for elt in seq:
if predicate(elt):
return elt
path = change['path'][0]
values = change['value']
current_values = getattr(package, path)
for value in values:
if value not in [i.name for i in current_values]:
msg = _("Value '{0}' of property '{1}' "
"does not exist.").format(value, path)
LOG.error(msg)
raise exc.HTTPNotFound(msg)
if path == 'categories' and len(current_values) == 1:
msg = _("At least one category should be assigned to the package")
LOG.error(msg)
raise exc.HTTPBadRequest(msg)
item_to_remove = find(current_values, lambda i: i.name == value)
current_values.remove(item_to_remove)
return package
def package_update(pkg_id, changes, context):
"""
Update package information
:param changes: parameters to update
:returns: detailed information about new package, dict
"""
operation_methods = {'add': _do_add,
'replace': _do_replace,
'remove': _do_remove}
session = db_session.get_session()
with session.begin():
pkg = _package_get(pkg_id, session)
_authorize_package(pkg, context)
for change in changes:
pkg = operation_methods[change['op']](pkg, change)
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")
LOG.error(msg)
raise exc.HTTPBadRequest(explanation=msg)
if value < 0:
msg = _("limit param must be positive")
LOG.error(msg)
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 'type' in filters.keys():
query = query.filter(pkg.type == filters['type'].title())
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_name' in filters.keys():
query = query.filter(pkg.class_definitions.any(
models.Class.name == filters['class_name']))
if 'fqn' in filters.keys():
query = query.filter(pkg.fully_qualified_name == filters['fqn'])
if 'search' in filters.keys():
conditions = []
search_str = filters['search']
for delimiter in ',;':
search_str = search_str.replace(delimiter, ' ')
key_words = ['%{0}%'.format(word) for word in search_str.split()]
relationships = inspection.inspect(pkg).relationships
searchable_attrs = set([
'fully_qualified_name', 'author', 'name', 'description', 'tags',
'categories', 'class_definitions'])
searchable_pkg_attrs = set(dir(pkg)) & searchable_attrs
for attr_name in searchable_pkg_attrs:
attr = getattr(pkg, attr_name)
if isinstance(attr, attributes.InstrumentedAttribute):
if attr_name in relationships:
model = relationships[attr_name].mapper.class_
for key_word in key_words:
condition = attr.any(model.name.like(key_word))
conditions.append(condition)
else:
for key_word in key_words:
conditions.append(attr.like(key_word))
query = query.filter(or_(*conditions))
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()
def package_upload(values, tenant_id):
"""
Upload a package with new application
:param values: parameters describing the new package
:returns: detailed information about new package, dict
"""
session = db_session.get_session()
package = models.Package()
composite_attr_to_func = {'categories': _get_categories,
'tags': _get_tags,
'class_definitions': _get_class_definitions}
with session.begin():
for attr, func in composite_attr_to_func.iteritems():
if values.get(attr):
result = func(values[attr], session)
setattr(package, attr, result)
del values[attr]
package.update(values)
package.owner_id = tenant_id
package.save(session)
return package
def package_delete(package_id):
"""
Delete package information from the system ID of a package, string
parameters to update
"""
session = db_session.get_session()
with session.begin():
package = session.query(models.Package).get(package_id)
session.delete(package)
def categories_list():
session = db_session.get_session()
return session.query(models.Category).all()
def category_get_names():
session = db_session.get_session()
categories = []
for row in session.query(models.Category.name).all():
for name in row:
categories.append(name)
return categories
def category_add(category_name):
session = db_session.get_session()
category = models.Category()
with session.begin():
category.update({'name': category_name})
category.save(session)
return category