Implement the GET and DELETE RESTAPI of resource plans
Change-Id: Ibe5e37801c7a89a1137cedea2a5f11261912b851 Closes-Bug: #1541729
This commit is contained in:
parent
2ae80a66a5
commit
78244d1e0a
@ -8,5 +8,6 @@
|
|||||||
"plan:create": "rule:admin_or_owner",
|
"plan:create": "rule:admin_or_owner",
|
||||||
"plan:update": "rule:admin_or_owner",
|
"plan:update": "rule:admin_or_owner",
|
||||||
"plan:delete": "rule:admin_or_owner",
|
"plan:delete": "rule:admin_or_owner",
|
||||||
"plan:get": "rule:admin_or_owner"
|
"plan:get": "rule:admin_or_owner",
|
||||||
|
"plan:get_all": "rule:admin_or_owner"
|
||||||
}
|
}
|
||||||
|
@ -12,18 +12,34 @@
|
|||||||
|
|
||||||
"""The plans api."""
|
"""The plans api."""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import webob
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
import smaug
|
import smaug
|
||||||
|
from smaug.api import common
|
||||||
from smaug.api.openstack import wsgi
|
from smaug.api.openstack import wsgi
|
||||||
from smaug import exception
|
from smaug import exception
|
||||||
from smaug.i18n import _, _LI
|
from smaug.i18n import _, _LI
|
||||||
|
|
||||||
from smaug import objects
|
from smaug import objects
|
||||||
from smaug.objects import base as objects_base
|
from smaug.objects import base as objects_base
|
||||||
from smaug.operationengine import api as operationengine_api
|
from smaug.operationengine import api as operationengine_api
|
||||||
import smaug.policy
|
import smaug.policy
|
||||||
|
from smaug import utils
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
query_plan_filters_opt = cfg.ListOpt('query_plan_filters',
|
||||||
|
default=['name', 'status'],
|
||||||
|
help="Plan filter options which "
|
||||||
|
"non-admin user could use to "
|
||||||
|
"query plans. Default values "
|
||||||
|
"are: ['name', 'status']")
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opt(query_plan_filters_opt)
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -45,7 +61,7 @@ def check_policy(context, action, target_obj=None):
|
|||||||
smaug.policy.enforce(context, _action, target)
|
smaug.policy.enforce(context, _action, target)
|
||||||
|
|
||||||
|
|
||||||
class PlanViewBuilder(object):
|
class PlanViewBuilder(common.ViewBuilder):
|
||||||
"""Model a server API response as a python dictionary."""
|
"""Model a server API response as a python dictionary."""
|
||||||
|
|
||||||
_collection_name = "plans"
|
_collection_name = "plans"
|
||||||
@ -67,6 +83,36 @@ class PlanViewBuilder(object):
|
|||||||
}
|
}
|
||||||
return plan_ref
|
return plan_ref
|
||||||
|
|
||||||
|
def detail_list(self, request, plans, plan_count=None):
|
||||||
|
"""Detailed view of a list of plans."""
|
||||||
|
return self._list_view(self.detail, request, plans,
|
||||||
|
plan_count,
|
||||||
|
self._collection_name)
|
||||||
|
|
||||||
|
def _list_view(self, func, request, plans, plan_count,
|
||||||
|
coll_name=_collection_name):
|
||||||
|
"""Provide a view for a list of plans.
|
||||||
|
|
||||||
|
:param func: Function used to format the plan data
|
||||||
|
:param request: API request
|
||||||
|
:param plans: List of plans in dictionary format
|
||||||
|
:param plan_count: Length of the original list of plans
|
||||||
|
:param coll_name: Name of collection, used to generate the next link
|
||||||
|
for a pagination query
|
||||||
|
:returns: Plan data in dictionary format
|
||||||
|
"""
|
||||||
|
plans_list = [func(request, plan)['plan'] for plan in plans]
|
||||||
|
plans_links = self._get_collection_links(request,
|
||||||
|
plans,
|
||||||
|
coll_name,
|
||||||
|
plan_count)
|
||||||
|
plans_dict = {}
|
||||||
|
plans_dict['plans'] = plans_list
|
||||||
|
if plans_links:
|
||||||
|
plans_dict['plans_links'] = plans_links
|
||||||
|
|
||||||
|
return plans_dict
|
||||||
|
|
||||||
|
|
||||||
class PlansController(wsgi.Controller):
|
class PlansController(wsgi.Controller):
|
||||||
"""The Plans API controller for the OpenStack API."""
|
"""The Plans API controller for the OpenStack API."""
|
||||||
@ -80,9 +126,21 @@ class PlansController(wsgi.Controller):
|
|||||||
def show(self, req, id):
|
def show(self, req, id):
|
||||||
"""Return data about the given plan."""
|
"""Return data about the given plan."""
|
||||||
context = req.environ['smaug.context']
|
context = req.environ['smaug.context']
|
||||||
|
|
||||||
LOG.info(_LI("Show plan with id: %s"), id, context=context)
|
LOG.info(_LI("Show plan with id: %s"), id, context=context)
|
||||||
# TODO(chenying)
|
|
||||||
return {'Smaug': "Plans show test."}
|
if not uuidutils.is_uuid_like(id):
|
||||||
|
msg = _("Invalid plan id provided.")
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
plan = self._plan_get(context, id)
|
||||||
|
except exception.PlanNotFound as error:
|
||||||
|
raise exc.HTTPNotFound(explanation=error.msg)
|
||||||
|
|
||||||
|
LOG.info(_LI("Show plan request issued successfully."),
|
||||||
|
resource={'id': plan.id})
|
||||||
|
return self._view_builder.detail(req, plan)
|
||||||
|
|
||||||
def delete(self, req, id):
|
def delete(self, req, id):
|
||||||
"""Delete a plan."""
|
"""Delete a plan."""
|
||||||
@ -90,22 +148,86 @@ class PlansController(wsgi.Controller):
|
|||||||
|
|
||||||
LOG.info(_LI("Delete plan with id: %s"), id, context=context)
|
LOG.info(_LI("Delete plan with id: %s"), id, context=context)
|
||||||
|
|
||||||
# TODO(chenying)
|
try:
|
||||||
return webob.Response(status_int=202)
|
plan = self._plan_get(context, id)
|
||||||
|
except exception.PlanNotFound as error:
|
||||||
|
raise exc.HTTPNotFound(explanation=error.msg)
|
||||||
|
|
||||||
|
check_policy(context, 'delete', plan)
|
||||||
|
plan.destroy()
|
||||||
|
LOG.info(_LI("Delete plan request issued successfully."),
|
||||||
|
resource={'id': plan.id})
|
||||||
|
|
||||||
def index(self, req):
|
def index(self, req):
|
||||||
"""Returns a summary list of plans."""
|
"""Returns a list of plans, transformed through view builder."""
|
||||||
|
context = req.environ['smaug.context']
|
||||||
|
|
||||||
# TODO(chenying)
|
LOG.info(_LI("Show plan list"), context=context)
|
||||||
|
|
||||||
return {'plan': "Plans index test."}
|
params = req.params.copy()
|
||||||
|
marker, limit, offset = common.get_pagination_params(params)
|
||||||
|
sort_keys, sort_dirs = common.get_sort_params(params)
|
||||||
|
filters = params
|
||||||
|
|
||||||
def detail(self, req):
|
utils.remove_invalid_filter_options(context,
|
||||||
"""Returns a detailed list of plans."""
|
filters,
|
||||||
|
self._get_plan_filter_options())
|
||||||
|
|
||||||
# TODO(chenying)
|
utils.check_filters(filters)
|
||||||
|
plans = self._get_all(context, marker, limit,
|
||||||
|
sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs,
|
||||||
|
filters=filters,
|
||||||
|
offset=offset)
|
||||||
|
|
||||||
return {'plan': "Plans detail test."}
|
retval_plans = self._view_builder.detail_list(req, plans)
|
||||||
|
|
||||||
|
LOG.info(_LI("Show plan list request issued successfully."))
|
||||||
|
|
||||||
|
return retval_plans
|
||||||
|
|
||||||
|
def _get_all(self, context, marker=None, limit=None, sort_keys=None,
|
||||||
|
sort_dirs=None, filters=None, offset=None):
|
||||||
|
check_policy(context, 'get_all')
|
||||||
|
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
all_tenants = utils.get_bool_param('all_tenants', filters)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if limit is not None:
|
||||||
|
limit = int(limit)
|
||||||
|
if limit < 0:
|
||||||
|
msg = _('limit param must be positive')
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
except ValueError:
|
||||||
|
msg = _('limit param must be an integer')
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
LOG.debug("Searching by: %s.", six.text_type(filters))
|
||||||
|
|
||||||
|
if context.is_admin and all_tenants:
|
||||||
|
# Need to remove all_tenants to pass the filtering below.
|
||||||
|
del filters['all_tenants']
|
||||||
|
plans = objects.PlanList.get_all(context, marker, limit,
|
||||||
|
sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs,
|
||||||
|
filters=filters,
|
||||||
|
offset=offset)
|
||||||
|
else:
|
||||||
|
plans = objects.PlanList.get_all_by_project(
|
||||||
|
context, context.project_id, marker, limit,
|
||||||
|
sort_keys=sort_keys, sort_dirs=sort_dirs, filters=filters,
|
||||||
|
offset=offset)
|
||||||
|
|
||||||
|
LOG.info(_LI("Get all plans completed successfully."))
|
||||||
|
return plans
|
||||||
|
|
||||||
|
def _get_plan_filter_options(self):
|
||||||
|
"""Return plan search options allowed by non-admin."""
|
||||||
|
return CONF.query_plan_filters
|
||||||
|
|
||||||
def create(self, req, body):
|
def create(self, req, body):
|
||||||
"""Creates a new plan."""
|
"""Creates a new plan."""
|
||||||
@ -153,6 +275,10 @@ class PlansController(wsgi.Controller):
|
|||||||
msg = _("Missing required element '%s' in request body") % 'plan'
|
msg = _("Missing required element '%s' in request body") % 'plan'
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
if not uuidutils.is_uuid_like(id):
|
||||||
|
msg = _("Invalid plan id provided.")
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
plan = body['plan']
|
plan = body['plan']
|
||||||
update_dict = {}
|
update_dict = {}
|
||||||
|
|
||||||
@ -186,6 +312,10 @@ class PlansController(wsgi.Controller):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
def _plan_get(self, context, plan_id):
|
def _plan_get(self, context, plan_id):
|
||||||
|
if not uuidutils.is_uuid_like(plan_id):
|
||||||
|
msg = _("Invalid plan id provided.")
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
plan = objects.Plan.get_by_id(context, plan_id)
|
plan = objects.Plan.get_by_id(context, plan_id)
|
||||||
try:
|
try:
|
||||||
check_policy(context, 'get', plan)
|
check_policy(context, 'get', plan)
|
||||||
|
@ -26,7 +26,7 @@ class APIRouter(wsgi_common.Router):
|
|||||||
scheduled_operation_resources = scheduled_operations.create_resource()
|
scheduled_operation_resources = scheduled_operations.create_resource()
|
||||||
mapper.resource("plan", "plans",
|
mapper.resource("plan", "plans",
|
||||||
controller=plans_resources,
|
controller=plans_resources,
|
||||||
collection={'detail': 'GET'},
|
collection={},
|
||||||
member={'action': 'POST'})
|
member={'action': 'POST'})
|
||||||
mapper.resource("scheduled_operation", "scheduled_operations",
|
mapper.resource("scheduled_operation", "scheduled_operations",
|
||||||
controller=scheduled_operation_resources,
|
controller=scheduled_operation_resources,
|
||||||
|
@ -391,3 +391,14 @@ def plan_get_all(context, marker, limit, sort_keys=None, sort_dirs=None,
|
|||||||
return IMPL.plan_get_all(context, marker, limit, sort_keys=sort_keys,
|
return IMPL.plan_get_all(context, marker, limit, sort_keys=sort_keys,
|
||||||
sort_dirs=sort_dirs, filters=filters,
|
sort_dirs=sort_dirs, filters=filters,
|
||||||
offset=offset)
|
offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
def plan_get_all_by_project(context, project_id, marker, limit,
|
||||||
|
sort_keys=None, sort_dirs=None, filters=None,
|
||||||
|
offset=None):
|
||||||
|
"""Get all plans belonging to a project."""
|
||||||
|
return IMPL.plan_get_all_by_project(context, project_id, marker, limit,
|
||||||
|
sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs,
|
||||||
|
filters=filters,
|
||||||
|
offset=offset)
|
||||||
|
@ -24,9 +24,12 @@ from oslo_db import api as oslo_db_api
|
|||||||
from oslo_db import exception as db_exc
|
from oslo_db import exception as db_exc
|
||||||
from oslo_db import options
|
from oslo_db import options
|
||||||
from oslo_db.sqlalchemy import session as db_session
|
from oslo_db.sqlalchemy import session as db_session
|
||||||
|
from oslo_db.sqlalchemy import utils as sqlalchemyutils
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
from sqlalchemy.orm import RelationshipProperty
|
||||||
from sqlalchemy.sql.expression import literal_column
|
from sqlalchemy.sql.expression import literal_column
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
@ -683,3 +686,265 @@ def plan_resources_update(context, plan_id, resources):
|
|||||||
return _plan_resources_update(context,
|
return _plan_resources_update(context,
|
||||||
plan_id,
|
plan_id,
|
||||||
resources)
|
resources)
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def plan_get_all(context, marker, limit, sort_keys=None, sort_dirs=None,
|
||||||
|
filters=None, offset=None):
|
||||||
|
"""Retrieves all plans.
|
||||||
|
|
||||||
|
If no sort parameters are specified then the returned plans are sorted
|
||||||
|
first by the 'created_at' key and then by the 'id' key in descending
|
||||||
|
order.
|
||||||
|
|
||||||
|
:param context: context to query under
|
||||||
|
:param marker: the last item of the previous page, used to determine the
|
||||||
|
next page of results to return
|
||||||
|
:param limit: maximum number of items to return
|
||||||
|
:param sort_keys: list of attributes by which results should be sorted,
|
||||||
|
paired with corresponding item in sort_dirs
|
||||||
|
:param sort_dirs: list of directions in which results should be sorted,
|
||||||
|
paired with corresponding item in sort_keys
|
||||||
|
:param filters: dictionary of filters; values that are in lists, tuples,
|
||||||
|
or sets cause an 'IN' operation, while exact matching
|
||||||
|
is used for other values, see _process_plan_filters
|
||||||
|
function for more information
|
||||||
|
:returns: list of matching plans
|
||||||
|
"""
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
# Generate the query
|
||||||
|
query = _generate_paginate_query(context, session, marker, limit,
|
||||||
|
sort_keys, sort_dirs, filters, offset)
|
||||||
|
# No plans would match, return empty list
|
||||||
|
if query is None:
|
||||||
|
return []
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def plan_get_all_by_project(context, project_id, marker, limit,
|
||||||
|
sort_keys=None, sort_dirs=None, filters=None,
|
||||||
|
offset=None):
|
||||||
|
"""Retrieves all plans in a project.
|
||||||
|
|
||||||
|
If no sort parameters are specified then the returned plans are sorted
|
||||||
|
first by the 'created_at' key and then by the 'id' key in descending
|
||||||
|
order.
|
||||||
|
|
||||||
|
:param context: context to query under
|
||||||
|
:param project_id: project for all plans being retrieved
|
||||||
|
:param marker: the last item of the previous page, used to determine the
|
||||||
|
next page of results to return
|
||||||
|
:param limit: maximum number of items to return
|
||||||
|
:param sort_keys: list of attributes by which results should be sorted,
|
||||||
|
paired with corresponding item in sort_dirs
|
||||||
|
:param sort_dirs: list of directions in which results should be sorted,
|
||||||
|
paired with corresponding item in sort_keys
|
||||||
|
:param filters: dictionary of filters; values that are in lists, tuples,
|
||||||
|
or sets cause an 'IN' operation, while exact matching
|
||||||
|
is used for other values, see _process_plan_filters
|
||||||
|
function for more information
|
||||||
|
:returns: list of matching plans
|
||||||
|
"""
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
authorize_project_context(context, project_id)
|
||||||
|
# Add in the project filter without modifying the given filters
|
||||||
|
filters = filters.copy() if filters else {}
|
||||||
|
filters['project_id'] = project_id
|
||||||
|
# Generate the query
|
||||||
|
query = _generate_paginate_query(context, session, marker, limit,
|
||||||
|
sort_keys, sort_dirs, filters, offset)
|
||||||
|
# No plans would match, return empty list
|
||||||
|
if query is None:
|
||||||
|
return []
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def _process_plan_filters(query, filters):
|
||||||
|
"""Common filter processing for Plan queries.
|
||||||
|
|
||||||
|
Filter values that are in lists, tuples, or sets cause an 'IN' operator
|
||||||
|
to be used, while exact matching ('==' operator) is used for other values.
|
||||||
|
|
||||||
|
A 'metadata' filter key must correspond to a dictionary value of metadata
|
||||||
|
key-value pairs.
|
||||||
|
|
||||||
|
:param query: Model query to use
|
||||||
|
:param filters: dictionary of filters
|
||||||
|
:returns: updated query or None
|
||||||
|
"""
|
||||||
|
filters = filters.copy()
|
||||||
|
|
||||||
|
# Apply exact match filters for everything else, ensure that the
|
||||||
|
# filter value exists on the model
|
||||||
|
for key in filters.keys():
|
||||||
|
# metadata is unique, must be a dict
|
||||||
|
if key == 'resources':
|
||||||
|
if not isinstance(filters[key], dict):
|
||||||
|
LOG.debug("'metadata' filter value is not valid.")
|
||||||
|
return None
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
column_attr = getattr(models.Plan, key)
|
||||||
|
# Do not allow relationship properties since those require
|
||||||
|
# schema specific knowledge
|
||||||
|
prop = getattr(column_attr, 'property')
|
||||||
|
if isinstance(prop, RelationshipProperty):
|
||||||
|
LOG.debug(("'%s' filter key is not valid, "
|
||||||
|
"it maps to a relationship."), key)
|
||||||
|
return None
|
||||||
|
except AttributeError:
|
||||||
|
LOG.debug("'%s' filter key is not valid.", key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Holds the simple exact matches
|
||||||
|
filter_dict = {}
|
||||||
|
|
||||||
|
# Iterate over all filters, special case the filter if necessary
|
||||||
|
for key, value in filters.items():
|
||||||
|
if key == 'resources':
|
||||||
|
col_attr = getattr(models.Plan, 'continue')
|
||||||
|
for k, v in value.items():
|
||||||
|
query = query.filter(or_(col_attr.any(key=k, value=v)))
|
||||||
|
elif isinstance(value, (list, tuple, set, frozenset)):
|
||||||
|
# Looking for values in a list; apply to query directly
|
||||||
|
column_attr = getattr(models.Plan, key)
|
||||||
|
query = query.filter(column_attr.in_(value))
|
||||||
|
else:
|
||||||
|
# OK, simple exact match; save for later
|
||||||
|
filter_dict[key] = value
|
||||||
|
|
||||||
|
# Apply simple exact matches
|
||||||
|
if filter_dict:
|
||||||
|
query = query.filter_by(**filter_dict)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
###############################
|
||||||
|
|
||||||
|
|
||||||
|
PAGINATION_HELPERS = {
|
||||||
|
models.Plan: (_plan_get_query, _process_plan_filters, _plan_get)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###############################
|
||||||
|
|
||||||
|
def _generate_paginate_query(context, session, marker, limit, sort_keys,
|
||||||
|
sort_dirs, filters, offset=None,
|
||||||
|
paginate_type=models.Plan):
|
||||||
|
"""Generate the query to include the filters and the paginate options.
|
||||||
|
|
||||||
|
Returns a query with sorting / pagination criteria added or None
|
||||||
|
if the given filters will not yield any results.
|
||||||
|
|
||||||
|
:param context: context to query under
|
||||||
|
:param session: the session to use
|
||||||
|
:param marker: the last item of the previous page; we returns the next
|
||||||
|
results after this value.
|
||||||
|
:param limit: maximum number of items to return
|
||||||
|
:param sort_keys: list of attributes by which results should be sorted,
|
||||||
|
paired with corresponding item in sort_dirs
|
||||||
|
:param sort_dirs: list of directions in which results should be sorted,
|
||||||
|
paired with corresponding item in sort_keys
|
||||||
|
:param filters: dictionary of filters; values that are in lists, tuples,
|
||||||
|
or sets cause an 'IN' operation, while exact matching
|
||||||
|
is used for other values, see _process_plan_filters
|
||||||
|
function for more information
|
||||||
|
:param offset: number of items to skip
|
||||||
|
:param paginate_type: type of pagination to generate
|
||||||
|
:returns: updated query or None
|
||||||
|
"""
|
||||||
|
get_query, process_filters, get = PAGINATION_HELPERS[paginate_type]
|
||||||
|
|
||||||
|
sort_keys, sort_dirs = process_sort_params(sort_keys,
|
||||||
|
sort_dirs,
|
||||||
|
default_dir='desc')
|
||||||
|
query = get_query(context, session=session)
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
query = process_filters(query, filters)
|
||||||
|
if query is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
marker_object = None
|
||||||
|
if marker is not None:
|
||||||
|
marker_object = get(context, marker, session)
|
||||||
|
|
||||||
|
return sqlalchemyutils.paginate_query(query, paginate_type, limit,
|
||||||
|
sort_keys,
|
||||||
|
marker=marker_object,
|
||||||
|
sort_dirs=sort_dirs)
|
||||||
|
|
||||||
|
|
||||||
|
def process_sort_params(sort_keys, sort_dirs, default_keys=None,
|
||||||
|
default_dir='asc'):
|
||||||
|
"""Process the sort parameters to include default keys.
|
||||||
|
|
||||||
|
Creates a list of sort keys and a list of sort directions. Adds the default
|
||||||
|
keys to the end of the list if they are not already included.
|
||||||
|
|
||||||
|
When adding the default keys to the sort keys list, the associated
|
||||||
|
direction is:
|
||||||
|
1) The first element in the 'sort_dirs' list (if specified), else
|
||||||
|
2) 'default_dir' value (Note that 'asc' is the default value since this is
|
||||||
|
the default in sqlalchemy.utils.paginate_query)
|
||||||
|
|
||||||
|
:param sort_keys: List of sort keys to include in the processed list
|
||||||
|
:param sort_dirs: List of sort directions to include in the processed list
|
||||||
|
:param default_keys: List of sort keys that need to be included in the
|
||||||
|
processed list, they are added at the end of the list
|
||||||
|
if not already specified.
|
||||||
|
:param default_dir: Sort direction associated with each of the default
|
||||||
|
keys that are not supplied, used when they are added
|
||||||
|
to the processed list
|
||||||
|
:returns: list of sort keys, list of sort directions
|
||||||
|
:raise exception.InvalidInput: If more sort directions than sort keys
|
||||||
|
are specified or if an invalid sort
|
||||||
|
direction is specified
|
||||||
|
"""
|
||||||
|
if default_keys is None:
|
||||||
|
default_keys = ['created_at', 'id']
|
||||||
|
|
||||||
|
# Determine direction to use for when adding default keys
|
||||||
|
if sort_dirs and len(sort_dirs):
|
||||||
|
default_dir_value = sort_dirs[0]
|
||||||
|
else:
|
||||||
|
default_dir_value = default_dir
|
||||||
|
|
||||||
|
# Create list of keys (do not modify the input list)
|
||||||
|
if sort_keys:
|
||||||
|
result_keys = list(sort_keys)
|
||||||
|
else:
|
||||||
|
result_keys = []
|
||||||
|
|
||||||
|
# If a list of directions is not provided, use the default sort direction
|
||||||
|
# for all provided keys.
|
||||||
|
if sort_dirs:
|
||||||
|
result_dirs = []
|
||||||
|
# Verify sort direction
|
||||||
|
for sort_dir in sort_dirs:
|
||||||
|
if sort_dir not in ('asc', 'desc'):
|
||||||
|
msg = _("Unknown sort direction, must be 'desc' or 'asc'.")
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
result_dirs.append(sort_dir)
|
||||||
|
else:
|
||||||
|
result_dirs = [default_dir_value for _sort_key in result_keys]
|
||||||
|
|
||||||
|
# Ensure that the key and direction length match
|
||||||
|
while len(result_dirs) < len(result_keys):
|
||||||
|
result_dirs.append(default_dir_value)
|
||||||
|
# Unless more direction are specified, which is an error
|
||||||
|
if len(result_dirs) > len(result_keys):
|
||||||
|
msg = _("Sort direction array size exceeds sort key array size.")
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
|
||||||
|
# Ensure defaults are included
|
||||||
|
for key in default_keys:
|
||||||
|
if key not in result_keys:
|
||||||
|
result_keys.append(key)
|
||||||
|
result_dirs.append(default_dir_value)
|
||||||
|
|
||||||
|
return result_keys, result_dirs
|
||||||
|
@ -146,6 +146,10 @@ class Invalid(SmaugException):
|
|||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterValue(Invalid):
|
||||||
|
message = _("%(err)s")
|
||||||
|
|
||||||
|
|
||||||
class InvalidInput(Invalid):
|
class InvalidInput(Invalid):
|
||||||
message = _("Invalid input received: %(reason)s")
|
message = _("Invalid input received: %(reason)s")
|
||||||
|
|
||||||
|
@ -152,3 +152,15 @@ class PlanList(base.ObjectListBase, base.SmaugObject):
|
|||||||
expected_attrs = ['resources']
|
expected_attrs = ['resources']
|
||||||
return base.obj_make_list(context, cls(context), objects.Plan,
|
return base.obj_make_list(context, cls(context), objects.Plan,
|
||||||
plans, expected_attrs=expected_attrs)
|
plans, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def get_all_by_project(cls, context, project_id, marker, limit,
|
||||||
|
sort_keys=None, sort_dirs=None, filters=None,
|
||||||
|
offset=None):
|
||||||
|
plans = db.plan_get_all_by_project(context, project_id, marker,
|
||||||
|
limit, sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs,
|
||||||
|
filters=filters, offset=offset)
|
||||||
|
expected_attrs = ['resources']
|
||||||
|
return base.obj_make_list(context, cls(context), objects.Plan,
|
||||||
|
plans, expected_attrs=expected_attrs)
|
||||||
|
@ -25,7 +25,7 @@ CONF = cfg.CONF
|
|||||||
|
|
||||||
DEFAULT_NAME = 'My 3 tier application'
|
DEFAULT_NAME = 'My 3 tier application'
|
||||||
DEFAULT_PROVIDER_ID = 'efc6a88b-9096-4bb6-8634-cda182a6e12a'
|
DEFAULT_PROVIDER_ID = 'efc6a88b-9096-4bb6-8634-cda182a6e12a'
|
||||||
DEFAULT_STATUS = 'started'
|
DEFAULT_STATUS = 'suspended'
|
||||||
DEFAULT_PROJECT_ID = '39bb894794b741e982bd26144d2949f6'
|
DEFAULT_PROJECT_ID = '39bb894794b741e982bd26144d2949f6'
|
||||||
DEFAULT_RESOURCES = [{'id': 'key1',
|
DEFAULT_RESOURCES = [{'id': 'key1',
|
||||||
"type": "value1"}]
|
"type": "value1"}]
|
||||||
@ -92,15 +92,17 @@ class PlanApiTest(base.TestCase):
|
|||||||
plan = self._plan_in_request_body()
|
plan = self._plan_in_request_body()
|
||||||
body = {"planxx": plan}
|
body = {"planxx": plan}
|
||||||
req = fakes.HTTPRequest.blank('/v1/plans')
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
self.assertRaises(exc.HTTPBadRequest, self.controller.update,
|
self.assertRaises(
|
||||||
req, "1", body)
|
exc.HTTPBadRequest, self.controller.update,
|
||||||
|
req, "2a9ce1f3-cc1a-4516-9435-0ebb13caa398", body)
|
||||||
|
|
||||||
def test_plan_update_InvalidId(self):
|
def test_plan_update_InvalidId(self):
|
||||||
plan = self._plan_in_request_body()
|
plan = self._plan_in_request_body()
|
||||||
body = {"plan": plan}
|
body = {"plan": plan}
|
||||||
req = fakes.HTTPRequest.blank('/v1/plans')
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
self.assertRaises(exc.HTTPNotFound, self.controller.update,
|
self.assertRaises(
|
||||||
req, "1", body)
|
exc.HTTPNotFound, self.controller.update,
|
||||||
|
req, "2a9ce1f3-cc1a-4516-9435-0ebb13caa398", body)
|
||||||
|
|
||||||
def test_plan_update_InvalidResources(self):
|
def test_plan_update_InvalidResources(self):
|
||||||
plan = self._plan_in_request_body(name=DEFAULT_NAME,
|
plan = self._plan_in_request_body(name=DEFAULT_NAME,
|
||||||
@ -110,15 +112,55 @@ class PlanApiTest(base.TestCase):
|
|||||||
resources=[{'key1': 'value1'}])
|
resources=[{'key1': 'value1'}])
|
||||||
body = {"plan": plan}
|
body = {"plan": plan}
|
||||||
req = fakes.HTTPRequest.blank('/v1/plans')
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
self.assertRaises(exception.InvalidInput, self.controller.update,
|
self.assertRaises(
|
||||||
req, "1", body)
|
exception.InvalidInput, self.controller.update,
|
||||||
|
req, "2a9ce1f3-cc1a-4516-9435-0ebb13caa398", body)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'smaug.api.v1.plans.PlansController._get_all')
|
||||||
|
def test_plan_list_detail(self, moak_get_all):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
|
self.controller.index(req)
|
||||||
|
self.assertTrue(moak_get_all.called)
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'smaug.api.v1.plans.PlansController._plan_get')
|
||||||
|
def test_plan_show(self, moak_plan_get):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
|
self.controller.\
|
||||||
|
show(req, '2a9ce1f3-cc1a-4516-9435-0ebb13caa398')
|
||||||
|
self.assertTrue(moak_plan_get.called)
|
||||||
|
|
||||||
|
def test_plan_show_Invalid(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/plans/1')
|
||||||
|
self.assertRaises(
|
||||||
|
exc.HTTPBadRequest, self.controller.show,
|
||||||
|
req, "1")
|
||||||
|
|
||||||
|
@mock.patch(
|
||||||
|
'smaug.api.v1.plans.PlansController._plan_get')
|
||||||
|
def test_plan_delete(self, moak_plan_get):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
|
self.controller.\
|
||||||
|
show(req, '2a9ce1f3-cc1a-4516-9435-0ebb13caa398')
|
||||||
|
self.assertTrue(moak_plan_get.called)
|
||||||
|
|
||||||
|
def test_plan_delete_Invalid(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/plans/1')
|
||||||
|
self.assertRaises(
|
||||||
|
exc.HTTPBadRequest, self.controller.show,
|
||||||
|
req, "1")
|
||||||
|
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'smaug.api.v1.plans.check_policy')
|
'smaug.api.v1.plans.check_policy')
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
'smaug.api.v1.plans.PlansController._plan_get')
|
'smaug.api.v1.plans.PlansController._plan_get')
|
||||||
def test_plan_update_InvalidStatus(self, mock_plan_get, mock_check_policy):
|
def test_plan_update_InvalidStatus(self, mock_plan_get, mock_check_policy):
|
||||||
plan = self._plan_in_request_body()
|
plan = self._plan_in_request_body(name=DEFAULT_NAME,
|
||||||
|
provider_id=DEFAULT_PROVIDER_ID,
|
||||||
|
status="started",
|
||||||
|
project_id=DEFAULT_PROJECT_ID,
|
||||||
|
resources=DEFAULT_RESOURCES)
|
||||||
body = {"plan": plan}
|
body = {"plan": plan}
|
||||||
req = fakes.HTTPRequest.blank('/v1/plans')
|
req = fakes.HTTPRequest.blank('/v1/plans')
|
||||||
mock_plan_get.return_value = plan
|
mock_plan_get.return_value = plan
|
||||||
|
@ -28,10 +28,3 @@ class PlansRouterTestCase(base.TestCase):
|
|||||||
req.content_type = 'application/json'
|
req.content_type = 'application/json'
|
||||||
response = req.get_response(self.app)
|
response = req.get_response(self.app)
|
||||||
self.assertEqual(200, response.status_int)
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
def test_plans_detail(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/fakeproject/plans/detail')
|
|
||||||
req.method = 'GET'
|
|
||||||
req.content_type = 'application/json'
|
|
||||||
response = req.get_response(self.app)
|
|
||||||
self.assertEqual(200, response.status_int)
|
|
||||||
|
@ -11,11 +11,14 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""Utilities and helper functions."""
|
"""Utilities and helper functions."""
|
||||||
|
import ast
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import strutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from smaug import exception
|
from smaug import exception
|
||||||
@ -78,3 +81,43 @@ def service_is_up(service):
|
|||||||
elapsed = (timeutils.utcnow(with_timezone=True) -
|
elapsed = (timeutils.utcnow(with_timezone=True) -
|
||||||
last_heartbeat).total_seconds()
|
last_heartbeat).total_seconds()
|
||||||
return abs(elapsed) <= CONF.service_down_time
|
return abs(elapsed) <= CONF.service_down_time
|
||||||
|
|
||||||
|
|
||||||
|
def remove_invalid_filter_options(context, filters,
|
||||||
|
allowed_search_options):
|
||||||
|
"""Remove search options that are not valid for non-admin API/context."""
|
||||||
|
|
||||||
|
if context.is_admin:
|
||||||
|
# Allow all options
|
||||||
|
return
|
||||||
|
# Otherwise, strip out all unknown options
|
||||||
|
unknown_options = [opt for opt in filters
|
||||||
|
if opt not in allowed_search_options]
|
||||||
|
bad_options = ", ".join(unknown_options)
|
||||||
|
LOG.debug("Removing options '%s' from query.", bad_options)
|
||||||
|
for opt in unknown_options:
|
||||||
|
del filters[opt]
|
||||||
|
|
||||||
|
|
||||||
|
def check_filters(filters):
|
||||||
|
for k, v in six.iteritems(filters):
|
||||||
|
try:
|
||||||
|
filters[k] = ast.literal_eval(v)
|
||||||
|
except (ValueError, SyntaxError):
|
||||||
|
LOG.debug('Could not evaluate value %s, assuming string', v)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_boolstr(val):
|
||||||
|
"""Check if the provided string is a valid bool string or not."""
|
||||||
|
val = str(val).lower()
|
||||||
|
return val in ('true', 'false', 'yes', 'no', 'y', 'n', '1', '0')
|
||||||
|
|
||||||
|
|
||||||
|
def get_bool_param(param_string, params):
|
||||||
|
param = params.get(param_string, False)
|
||||||
|
if not is_valid_boolstr(param):
|
||||||
|
msg = _('Value %(param)s for %(param_string)s is not a '
|
||||||
|
'boolean.') % {'param': param, 'param_string': param_string}
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
return strutils.bool_from_string(param, strict=True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user