From 78244d1e0a720c0b365cbff3753953f1a047a289 Mon Sep 17 00:00:00 2001 From: chenying Date: Mon, 15 Feb 2016 11:31:34 +0800 Subject: [PATCH] Implement the GET and DELETE RESTAPI of resource plans Change-Id: Ibe5e37801c7a89a1137cedea2a5f11261912b851 Closes-Bug: #1541729 --- etc/policy.json | 3 +- smaug/api/v1/plans.py | 156 +++++++++++++-- smaug/api/v1/router.py | 2 +- smaug/db/api.py | 11 + smaug/db/sqlalchemy/api.py | 265 +++++++++++++++++++++++++ smaug/exception.py | 4 + smaug/objects/plan.py | 12 ++ smaug/tests/unit/api/v1/test_plans.py | 58 +++++- smaug/tests/unit/api/v1/test_router.py | 7 - smaug/utils.py | 43 ++++ 10 files changed, 531 insertions(+), 30 deletions(-) diff --git a/etc/policy.json b/etc/policy.json index 67c818bd..20df4141 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -8,5 +8,6 @@ "plan:create": "rule:admin_or_owner", "plan:update": "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" } diff --git a/smaug/api/v1/plans.py b/smaug/api/v1/plans.py index e0a812cc..3514d726 100644 --- a/smaug/api/v1/plans.py +++ b/smaug/api/v1/plans.py @@ -12,18 +12,34 @@ """The plans api.""" +from oslo_config import cfg from oslo_log import log as logging -import webob +from oslo_utils import uuidutils + from webob import exc import smaug +from smaug.api import common from smaug.api.openstack import wsgi from smaug import exception from smaug.i18n import _, _LI + from smaug import objects from smaug.objects import base as objects_base from smaug.operationengine import api as operationengine_api 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__) @@ -45,7 +61,7 @@ def check_policy(context, action, target_obj=None): smaug.policy.enforce(context, _action, target) -class PlanViewBuilder(object): +class PlanViewBuilder(common.ViewBuilder): """Model a server API response as a python dictionary.""" _collection_name = "plans" @@ -67,6 +83,36 @@ class PlanViewBuilder(object): } 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): """The Plans API controller for the OpenStack API.""" @@ -80,9 +126,21 @@ class PlansController(wsgi.Controller): def show(self, req, id): """Return data about the given plan.""" context = req.environ['smaug.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): """Delete a plan.""" @@ -90,22 +148,86 @@ class PlansController(wsgi.Controller): LOG.info(_LI("Delete plan with id: %s"), id, context=context) - # TODO(chenying) - return webob.Response(status_int=202) + try: + 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): - """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): - """Returns a detailed list of plans.""" + utils.remove_invalid_filter_options(context, + 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): """Creates a new plan.""" @@ -153,6 +275,10 @@ class PlansController(wsgi.Controller): msg = _("Missing required element '%s' in request body") % 'plan' 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'] update_dict = {} @@ -186,6 +312,10 @@ class PlansController(wsgi.Controller): return retval 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) try: check_policy(context, 'get', plan) diff --git a/smaug/api/v1/router.py b/smaug/api/v1/router.py index 978ccff6..ec68bb8d 100644 --- a/smaug/api/v1/router.py +++ b/smaug/api/v1/router.py @@ -26,7 +26,7 @@ class APIRouter(wsgi_common.Router): scheduled_operation_resources = scheduled_operations.create_resource() mapper.resource("plan", "plans", controller=plans_resources, - collection={'detail': 'GET'}, + collection={}, member={'action': 'POST'}) mapper.resource("scheduled_operation", "scheduled_operations", controller=scheduled_operation_resources, diff --git a/smaug/db/api.py b/smaug/db/api.py index 391ee63f..90672f6d 100644 --- a/smaug/db/api.py +++ b/smaug/db/api.py @@ -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, sort_dirs=sort_dirs, filters=filters, 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) diff --git a/smaug/db/sqlalchemy/api.py b/smaug/db/sqlalchemy/api.py index 8ecca237..b1acb199 100644 --- a/smaug/db/sqlalchemy/api.py +++ b/smaug/db/sqlalchemy/api.py @@ -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 options 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_utils import timeutils +from sqlalchemy import or_ from sqlalchemy.orm import joinedload +from sqlalchemy.orm import RelationshipProperty from sqlalchemy.sql.expression import literal_column from sqlalchemy.sql import func @@ -683,3 +686,265 @@ def plan_resources_update(context, plan_id, resources): return _plan_resources_update(context, plan_id, 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 diff --git a/smaug/exception.py b/smaug/exception.py index 34417230..e8198fc5 100644 --- a/smaug/exception.py +++ b/smaug/exception.py @@ -146,6 +146,10 @@ class Invalid(SmaugException): code = 400 +class InvalidParameterValue(Invalid): + message = _("%(err)s") + + class InvalidInput(Invalid): message = _("Invalid input received: %(reason)s") diff --git a/smaug/objects/plan.py b/smaug/objects/plan.py index f27db441..b71f1e98 100644 --- a/smaug/objects/plan.py +++ b/smaug/objects/plan.py @@ -152,3 +152,15 @@ class PlanList(base.ObjectListBase, base.SmaugObject): expected_attrs = ['resources'] return base.obj_make_list(context, cls(context), objects.Plan, 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) diff --git a/smaug/tests/unit/api/v1/test_plans.py b/smaug/tests/unit/api/v1/test_plans.py index 1e62b6db..15cec0d3 100644 --- a/smaug/tests/unit/api/v1/test_plans.py +++ b/smaug/tests/unit/api/v1/test_plans.py @@ -25,7 +25,7 @@ CONF = cfg.CONF DEFAULT_NAME = 'My 3 tier application' DEFAULT_PROVIDER_ID = 'efc6a88b-9096-4bb6-8634-cda182a6e12a' -DEFAULT_STATUS = 'started' +DEFAULT_STATUS = 'suspended' DEFAULT_PROJECT_ID = '39bb894794b741e982bd26144d2949f6' DEFAULT_RESOURCES = [{'id': 'key1', "type": "value1"}] @@ -92,15 +92,17 @@ class PlanApiTest(base.TestCase): plan = self._plan_in_request_body() body = {"planxx": plan} req = fakes.HTTPRequest.blank('/v1/plans') - self.assertRaises(exc.HTTPBadRequest, self.controller.update, - req, "1", body) + self.assertRaises( + exc.HTTPBadRequest, self.controller.update, + req, "2a9ce1f3-cc1a-4516-9435-0ebb13caa398", body) def test_plan_update_InvalidId(self): plan = self._plan_in_request_body() body = {"plan": plan} req = fakes.HTTPRequest.blank('/v1/plans') - self.assertRaises(exc.HTTPNotFound, self.controller.update, - req, "1", body) + self.assertRaises( + exc.HTTPNotFound, self.controller.update, + req, "2a9ce1f3-cc1a-4516-9435-0ebb13caa398", body) def test_plan_update_InvalidResources(self): plan = self._plan_in_request_body(name=DEFAULT_NAME, @@ -110,15 +112,55 @@ class PlanApiTest(base.TestCase): resources=[{'key1': 'value1'}]) body = {"plan": plan} req = fakes.HTTPRequest.blank('/v1/plans') - self.assertRaises(exception.InvalidInput, self.controller.update, - req, "1", body) + self.assertRaises( + 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( 'smaug.api.v1.plans.check_policy') @mock.patch( 'smaug.api.v1.plans.PlansController._plan_get') 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} req = fakes.HTTPRequest.blank('/v1/plans') mock_plan_get.return_value = plan diff --git a/smaug/tests/unit/api/v1/test_router.py b/smaug/tests/unit/api/v1/test_router.py index 58ee4653..da4490a6 100644 --- a/smaug/tests/unit/api/v1/test_router.py +++ b/smaug/tests/unit/api/v1/test_router.py @@ -28,10 +28,3 @@ class PlansRouterTestCase(base.TestCase): req.content_type = 'application/json' response = req.get_response(self.app) 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) diff --git a/smaug/utils.py b/smaug/utils.py index 4a7d1f10..7fa742a9 100644 --- a/smaug/utils.py +++ b/smaug/utils.py @@ -11,11 +11,14 @@ # under the License. """Utilities and helper functions.""" +import ast import os from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import strutils from oslo_utils import timeutils + import six from smaug import exception @@ -78,3 +81,43 @@ def service_is_up(service): elapsed = (timeutils.utcnow(with_timezone=True) - last_heartbeat).total_seconds() 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)