404 lines
14 KiB
Python
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
|