From 0e51d26e1caeaa63d5d8d8a71528940979d9ff3d Mon Sep 17 00:00:00 2001 From: Xavier Hardy Date: Tue, 21 Jun 2016 09:28:33 +0200 Subject: [PATCH] Fix API inconsistencies with GET /v2/workflows Add missing API parameters to Workbooks, Actions, Executions, Cron Triggers, Environments, Action Executions and Tasks: marker, limit, sort_keys, sort_dirs, fields, filters Change-Id: I207bc93e84067f83f926843f0b9895a19b831079 Partial-Bug: #1585646 --- mistral/api/controllers/v2/action.py | 43 +++-- .../api/controllers/v2/action_execution.py | 144 ++++++++++++++--- mistral/api/controllers/v2/cron_trigger.py | 55 +++++-- mistral/api/controllers/v2/environment.py | 53 ++++-- mistral/api/controllers/v2/execution.py | 47 +++--- mistral/api/controllers/v2/task.py | 139 +++++++++++++--- mistral/api/controllers/v2/workbook.py | 52 ++++-- mistral/api/controllers/v2/workflow.py | 48 ++---- mistral/db/v2/api.py | 35 +++- mistral/db/v2/sqlalchemy/api.py | 153 +++++++++--------- .../unit/api/v2/test_action_executions.py | 8 +- mistral/utils/rest_utils.py | 96 +++++++++++ 12 files changed, 627 insertions(+), 246 deletions(-) diff --git a/mistral/api/controllers/v2/action.py b/mistral/api/controllers/v2/action.py index 4e4d8719..327e8373 100644 --- a/mistral/api/controllers/v2/action.py +++ b/mistral/api/controllers/v2/action.py @@ -182,9 +182,9 @@ class ActionsController(rest.RestController, hooks.HookController): db_api.delete_action_definition(name) @wsme_pecan.wsexpose(Actions, types.uuid, int, types.uniquelist, - types.list) + types.list, types.uniquelist, types.jsontype) def get_all(self, marker=None, limit=None, sort_keys='name', - sort_dirs='asc'): + sort_dirs='asc', fields='', **filters): """Return all actions. :param marker: Optional. Pagination marker for large data sets. @@ -196,35 +196,30 @@ class ActionsController(rest.RestController, hooks.HookController): :param sort_dirs: Optional. Directions to sort corresponding to sort_keys, "asc" or "desc" can be choosed. Default: asc. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. Where project_id is the same as the requester or project_id is different but the scope is public. """ acl.enforce('actions:list', context.ctx()) LOG.info("Fetch actions. marker=%s, limit=%s, sort_keys=%s, " - "sort_dirs=%s", marker, limit, sort_keys, sort_dirs) + "sort_dirs=%s, filters=%s", marker, limit, sort_keys, + sort_dirs, filters) - rest_utils.validate_query_params(limit, sort_keys, sort_dirs) - - marker_obj = None - - if marker: - marker_obj = db_api.get_action_definition_by_id(marker) - - db_action_defs = db_api.get_action_definitions( + return rest_utils.get_all( + Actions, + Action, + db_api.get_action_definitions, + db_api.get_action_definition_by_id, + resource_function=None, + marker=marker, limit=limit, - marker=marker_obj, sort_keys=sort_keys, - sort_dirs=sort_dirs - ) - - actions_list = [Action.from_dict(db_model.to_dict()) - for db_model in db_action_defs] - - return Actions.convert_with_links( - actions_list, - limit, - pecan.request.host_url, - sort_keys=','.join(sort_keys), - sort_dirs=','.join(sort_dirs) + sort_dirs=sort_dirs, + fields=fields, + **filters ) diff --git a/mistral/api/controllers/v2/action_execution.py b/mistral/api/controllers/v2/action_execution.py index 03495eba..bcf5ace2 100644 --- a/mistral/api/controllers/v2/action_execution.py +++ b/mistral/api/controllers/v2/action_execution.py @@ -79,11 +79,16 @@ class ActionExecution(resource.Resource): ) -class ActionExecutions(resource.Resource): +class ActionExecutions(resource.ResourceList): """A collection of action_executions.""" action_executions = [ActionExecution] + def __init__(self, **kwargs): + self._type = 'action_executions' + + super(ActionExecutions, self).__init__(**kwargs) + @classmethod def sample(cls): return cls(action_executions=[ActionExecution.sample()]) @@ -115,18 +120,48 @@ def _get_action_execution_resource(action_ex): return res -def _get_action_executions(task_execution_id=None): - kwargs = {'type': 'action_execution'} +def _get_action_executions(task_execution_id=None, marker=None, limit=None, + sort_keys='created_at', sort_dirs='asc', + fields='', **filters): + """Return all action executions. + + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ + filters['type'] = 'action_execution' if task_execution_id: - kwargs['task_execution_id'] = task_execution_id + filters['task_execution_id'] = task_execution_id - action_execs = [ - _get_action_execution_resource(a_ex) - for a_ex in db_api.get_action_executions(**kwargs) - ] - - return ActionExecutions(action_executions=action_execs) + return rest_utils.get_all( + ActionExecutions, + ActionExecution, + db_api.get_action_executions, + db_api.get_action_execution, + resource_function=_get_action_execution_resource, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) class ActionExecutionsController(rest.RestController): @@ -194,13 +229,45 @@ class ActionExecutionsController(rest.RestController): return ActionExecution.from_dict(values) - @wsme_pecan.wsexpose(ActionExecutions) - def get_all(self): - """Return all action_executions within the execution.""" - acl.enforce('action_executions:list', context.ctx()) - LOG.info("Fetch action_executions") + @wsme_pecan.wsexpose(ActionExecutions, types.uuid, int, types.uniquelist, + types.list, types.uniquelist, types.jsontype) + def get_all(self, marker=None, limit=None, sort_keys='created_at', + sort_dirs='asc', fields='', **filters): + """Return all tasks within the execution. - return _get_action_executions() + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + + """ + acl.enforce('action_executions:list', context.ctx()) + LOG.info("Fetch action_executions. marker=%s, limit=%s, " + "sort_keys=%s, sort_dirs=%s, filters=%s", + marker, limit, sort_keys, sort_dirs, filters) + + return _get_action_executions( + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @@ -228,13 +295,46 @@ class ActionExecutionsController(rest.RestController): class TasksActionExecutionController(rest.RestController): - @wsme_pecan.wsexpose(ActionExecutions, wtypes.text) - def get_all(self, task_execution_id): - """Return all action executions within the task execution.""" - acl.enforce('action_executions:list', context.ctx()) - LOG.info("Fetch action executions") + @wsme_pecan.wsexpose(ActionExecutions, types.uuid, types.uuid, int, + types.uniquelist, types.list, types.uniquelist, + types.jsontype) + def get_all(self, task_execution_id, marker=None, limit=None, + sort_keys='created_at', sort_dirs='asc', fields='', **filters): + """Return all tasks within the execution. - return _get_action_executions(task_execution_id=task_execution_id) + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ + acl.enforce('action_executions:list', context.ctx()) + LOG.info("Fetch action_executions. marker=%s, limit=%s, " + "sort_keys=%s, sort_dirs=%s, filters=%s", + marker, limit, sort_keys, sort_dirs, filters) + + return _get_action_executions( + task_execution_id=task_execution_id, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(ActionExecution, wtypes.text, wtypes.text) diff --git a/mistral/api/controllers/v2/cron_trigger.py b/mistral/api/controllers/v2/cron_trigger.py index 5c60a8b2..a5aa3f17 100644 --- a/mistral/api/controllers/v2/cron_trigger.py +++ b/mistral/api/controllers/v2/cron_trigger.py @@ -64,11 +64,16 @@ class CronTrigger(resource.Resource): updated_at='1970-01-01T00:00:00.000000') -class CronTriggers(resource.Resource): +class CronTriggers(resource.ResourceList): """A collection of cron triggers.""" cron_triggers = [CronTrigger] + def __init__(self, **kwargs): + self._type = 'cron_triggers' + + super(CronTriggers, self).__init__(**kwargs) + @classmethod def sample(cls): return cls(cron_triggers=[CronTrigger.sample()]) @@ -119,16 +124,44 @@ class CronTriggersController(rest.RestController): db_api.delete_cron_trigger(name) - @wsme_pecan.wsexpose(CronTriggers) - def get_all(self): - """Return all cron triggers.""" + @wsme_pecan.wsexpose(CronTriggers, types.uuid, int, types.uniquelist, + types.list, types.uniquelist, types.jsontype) + def get_all(self, marker=None, limit=None, sort_keys='created_at', + sort_dirs='asc', fields='', **filters): + """Return all cron triggers. + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + + """ acl.enforce('cron_triggers:list', context.ctx()) - LOG.info("Fetch cron triggers.") + LOG.info("Fetch cron triggers. marker=%s, limit=%s, sort_keys=%s, " + "sort_dirs=%s, filters=%s", marker, limit, sort_keys, + sort_dirs, filters) - _list = [ - CronTrigger.from_dict(db_model.to_dict()) - for db_model in db_api.get_cron_triggers() - ] - - return CronTriggers(cron_triggers=_list) + return rest_utils.get_all( + CronTriggers, + CronTrigger, + db_api.get_cron_triggers, + db_api.get_cron_trigger, + resource_function=None, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) diff --git a/mistral/api/controllers/v2/environment.py b/mistral/api/controllers/v2/environment.py index 13bf9c5a..900d784f 100644 --- a/mistral/api/controllers/v2/environment.py +++ b/mistral/api/controllers/v2/environment.py @@ -61,33 +61,66 @@ class Environment(resource.Resource): updated_at='1970-01-01T00:00:00.000000') -class Environments(resource.Resource): +class Environments(resource.ResourceList): """A collection of Environment resources.""" environments = [Environment] + def __init__(self, **kwargs): + self._type = 'environments' + + super(Environments, self).__init__(**kwargs) + @classmethod def sample(cls): return cls(environments=[Environment.sample()]) class EnvironmentController(rest.RestController): - @wsme_pecan.wsexpose(Environments) - def get_all(self): + @wsme_pecan.wsexpose(Environments, types.uuid, int, types.uniquelist, + types.list, types.uniquelist, types.jsontype) + def get_all(self, marker=None, limit=None, sort_keys='created_at', + sort_dirs='asc', fields='', **filters): """Return all environments. Where project_id is the same as the requestor or project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ acl.enforce('environments:list', context.ctx()) - LOG.info("Fetch environments.") + LOG.info("Fetch environments. marker=%s, limit=%s, sort_keys=%s, " + "sort_dirs=%s, filters=%s", marker, limit, sort_keys, + sort_dirs, filters) - environments = [ - Environment.from_dict(db_model.to_dict()) - for db_model in db_api.get_environments() - ] - - return Environments(environments=environments) + return rest_utils.get_all( + Environments, + Environment, + db_api.get_environments, + db_api.get_environment, + resource_function=None, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(Environment, wtypes.text) diff --git a/mistral/api/controllers/v2/execution.py b/mistral/api/controllers/v2/execution.py index 982c2d51..a7550f9b 100644 --- a/mistral/api/controllers/v2/execution.py +++ b/mistral/api/controllers/v2/execution.py @@ -15,7 +15,6 @@ # limitations under the License. from oslo_log import log as logging -import pecan from pecan import rest from wsme import types as wtypes import wsmeext.pecan as wsme_pecan @@ -255,9 +254,9 @@ class ExecutionsController(rest.RestController): return db_api.delete_workflow_execution(id) @wsme_pecan.wsexpose(Executions, types.uuid, int, types.uniquelist, - types.list) + types.list, types.uniquelist, types.jsontype) def get_all(self, marker=None, limit=None, sort_keys='created_at', - sort_dirs='asc'): + sort_dirs='asc', fields='', **filters): """Return all Executions. :param marker: Optional. Pagination marker for large data sets. @@ -270,36 +269,30 @@ class ExecutionsController(rest.RestController): sort_keys, "asc" or "desc" can be chosen. Default: desc. The length of sort_dirs can be equal or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ acl.enforce('executions:list', context.ctx()) LOG.info( "Fetch executions. marker=%s, limit=%s, sort_keys=%s, " - "sort_dirs=%s", marker, limit, sort_keys, sort_dirs + "sort_dirs=%s, filters=%s", marker, limit, sort_keys, sort_dirs, + filters ) - rest_utils.validate_query_params(limit, sort_keys, sort_dirs) - - marker_obj = None - - if marker: - marker_obj = db_api.get_workflow_execution(marker) - - db_workflow_exs = db_api.get_workflow_executions( + return rest_utils.get_all( + Executions, + Execution, + db_api.get_workflow_executions, + db_api.get_workflow_execution, + resource_function=None, + marker=marker, limit=limit, - marker=marker_obj, sort_keys=sort_keys, - sort_dirs=sort_dirs - ) - - wf_executions = [ - Execution.from_dict(db_model.to_dict()) - for db_model in db_workflow_exs - ] - - return Executions.convert_with_links( - wf_executions, - limit, - pecan.request.host_url, - sort_keys=','.join(sort_keys), - sort_dirs=','.join(sort_dirs) + sort_dirs=sort_dirs, + fields=fields, + **filters ) diff --git a/mistral/api/controllers/v2/task.py b/mistral/api/controllers/v2/task.py index 8f74d733..322f310c 100644 --- a/mistral/api/controllers/v2/task.py +++ b/mistral/api/controllers/v2/task.py @@ -85,11 +85,16 @@ class Task(resource.Resource): ) -class Tasks(resource.Resource): +class Tasks(resource.ResourceList): """A collection of tasks.""" tasks = [Task] + def __init__(self, **kwargs): + self._type = 'tasks' + + super(Tasks, self).__init__(**kwargs) + @classmethod def sample(cls): return cls(tasks=[Task.sample()]) @@ -102,16 +107,46 @@ def _get_task_resource_with_result(task_ex): return task -def _get_task_resources_with_results(wf_ex_id=None): - filters = {} +def _get_task_resources_with_results(wf_ex_id=None, marker=None, limit=None, + sort_keys='created_at', sort_dirs='asc', + fields='', **filters): + """Return all tasks within the execution. + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ if wf_ex_id: filters['workflow_execution_id'] = wf_ex_id - task_exs = db_api.get_task_executions(**filters) - tasks = [_get_task_resource_with_result(t_e) for t_e in task_exs] - - return Tasks(tasks=tasks) + return rest_utils.get_all( + Tasks, + Task, + db_api.get_task_executions, + db_api.get_task_execution, + resource_function=_get_task_resource_with_result, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) class TasksController(rest.RestController): @@ -128,13 +163,45 @@ class TasksController(rest.RestController): return _get_task_resource_with_result(task_ex) - @wsme_pecan.wsexpose(Tasks) - def get_all(self): - """Return all tasks within the execution.""" - acl.enforce('tasks:list', context.ctx()) - LOG.info("Fetch tasks") + @wsme_pecan.wsexpose(Tasks, types.uuid, int, types.uniquelist, + types.list, types.uniquelist, types.jsontype) + def get_all(self, marker=None, limit=None, sort_keys='created_at', + sort_dirs='asc', fields='', **filters): + """Return all tasks. - return _get_task_resources_with_results() + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + + """ + acl.enforce('tasks:list', context.ctx()) + LOG.info("Fetch tasks. marker=%s, limit=%s, sort_keys=%s, " + "sort_dirs=%s, filters=%s", marker, limit, sort_keys, + sort_dirs, filters) + + return _get_task_resources_with_results( + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(Task, wtypes.text, body=Task) @@ -190,10 +257,44 @@ class TasksController(rest.RestController): class ExecutionTasksController(rest.RestController): - @wsme_pecan.wsexpose(Tasks, wtypes.text) - def get_all(self, workflow_execution_id): - """Return all tasks within the workflow execution.""" - acl.enforce('tasks:list', context.ctx()) - LOG.info("Fetch tasks.") - return _get_task_resources_with_results(workflow_execution_id) + @wsme_pecan.wsexpose(Tasks, types.uuid, types.uuid, int, types.uniquelist, + types.list, types.uniquelist) + def get_all(self, workflow_execution_id, marker=None, limit=None, + sort_keys='created_at', sort_dirs='asc', fields='', **filters): + """Return all tasks within the execution. + + Where project_id is the same as the requestor or + project_id is different but the scope is public. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at, which is backward compatible. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be chosen. + Default: desc. The length of sort_dirs can be equal + or less than that of sort_keys. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. + """ + acl.enforce('tasks:list', context.ctx()) + LOG.info("Fetch tasks. workflow_execution_id=%s, marker=%s, limit=%s, " + "sort_keys=%s, sort_dirs=%s, filters=%s", + workflow_execution_id, marker, limit, sort_keys, sort_dirs, + filters) + + return _get_task_resources_with_results( + wf_ex_id=workflow_execution_id, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) diff --git a/mistral/api/controllers/v2/workbook.py b/mistral/api/controllers/v2/workbook.py index 961b2a89..f0405eea 100644 --- a/mistral/api/controllers/v2/workbook.py +++ b/mistral/api/controllers/v2/workbook.py @@ -22,6 +22,7 @@ import wsmeext.pecan as wsme_pecan from mistral.api import access_control as acl from mistral.api.controllers import resource +from mistral.api.controllers.v2 import types from mistral.api.controllers.v2 import validation from mistral.api.hooks import content_type as ct_hook from mistral import context @@ -62,11 +63,16 @@ class Workbook(resource.Resource): updated_at='1970-01-01T00:00:00.000000') -class Workbooks(resource.Resource): +class Workbooks(resource.ResourceList): """A collection of Workbooks.""" workbooks = [Workbook] + def __init__(self, **kwargs): + self._type = 'workbooks' + + super(Workbooks, self).__init__(**kwargs) + @classmethod def sample(cls): return cls(workbooks=[Workbook.sample()]) @@ -123,17 +129,45 @@ class WorkbooksController(rest.RestController, hooks.HookController): db_api.delete_workbook(name) - @wsme_pecan.wsexpose(Workbooks) - def get_all(self): - """Return all workbooks. + @wsme_pecan.wsexpose(Workbooks, types.uuid, int, types.uniquelist, + types.list, types.uniquelist, types.jsontype) + def get_all(self, marker=None, limit=None, sort_keys='created_at', + sort_dirs='asc', fields='', **filters): + """Return a list of workbooks. + + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be choosed. + Default: asc. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. Where project_id is the same as the requestor or project_id is different but the scope is public. """ acl.enforce('workbooks:list', context.ctx()) - LOG.info("Fetch workbooks.") + LOG.info("Fetch workbooks. marker=%s, limit=%s, sort_keys=%s, " + "sort_dirs=%s, fields=%s, filters=%s", marker, limit, + sort_keys, sort_dirs, fields, filters) - workbooks_list = [Workbook.from_dict(db_model.to_dict()) - for db_model in db_api.get_workbooks()] - - return Workbooks(workbooks=workbooks_list) + return rest_utils.get_all( + Workbooks, + Workbook, + db_api.get_workbooks, + db_api.get_workbook, + resource_function=None, + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) diff --git a/mistral/api/controllers/v2/workflow.py b/mistral/api/controllers/v2/workflow.py index c80d671c..4e0e8575 100644 --- a/mistral/api/controllers/v2/workflow.py +++ b/mistral/api/controllers/v2/workflow.py @@ -236,9 +236,9 @@ class WorkflowsController(rest.RestController, hooks.HookController): @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(Workflows, types.uuid, int, types.uniquelist, - types.list, types.uniquelist) + types.list, types.uniquelist, types.jsontype) def get_all(self, marker=None, limit=None, sort_keys='created_at', - sort_dirs='asc', fields=''): + sort_dirs='asc', fields='', **filters): """Return a list of workflows. :param marker: Optional. Pagination marker for large data sets. @@ -254,46 +254,26 @@ class WorkflowsController(rest.RestController, hooks.HookController): be returned. 'id' will be included automatically in fields if it's provided, since it will be used when constructing 'next' link. + :param filters: Optional. A list of filters to apply to the result. Where project_id is the same as the requester or project_id is different but the scope is public. """ acl.enforce('workflows:list', context.ctx()) LOG.info("Fetch workflows. marker=%s, limit=%s, sort_keys=%s, " - "sort_dirs=%s, fields=%s", marker, limit, sort_keys, - sort_dirs, fields) + "sort_dirs=%s, fields=%s, filters=%s", marker, limit, + sort_keys, sort_dirs, fields, filters) - if fields and 'id' not in fields: - fields.insert(0, 'id') - - rest_utils.validate_query_params(limit, sort_keys, sort_dirs) - rest_utils.validate_fields(fields, Workflow.get_fields()) - - marker_obj = None - - if marker: - marker_obj = db_api.get_workflow_definition_by_id(marker) - - db_workflows = db_api.get_workflow_definitions( + return rest_utils.get_all( + Workflows, + Workflow, + db_api.get_workflow_definitions, + db_api.get_workflow_definition_by_id, + resource_function=None, + marker=marker, limit=limit, - marker=marker_obj, sort_keys=sort_keys, sort_dirs=sort_dirs, - fields=fields - ) - - workflows_list = [] - - for data in db_workflows: - workflow_dict = (dict(zip(fields, data)) if fields else - data.to_dict()) - workflows_list.append(Workflow.from_dict(workflow_dict)) - - return Workflows.convert_with_links( - workflows_list, - limit, - pecan.request.host_url, - sort_keys=','.join(sort_keys), - sort_dirs=','.join(sort_dirs), - fields=','.join(fields) if fields else '' + fields=fields, + **filters ) diff --git a/mistral/db/v2/api.py b/mistral/db/v2/api.py index f0f58902..badd1e14 100644 --- a/mistral/db/v2/api.py +++ b/mistral/db/v2/api.py @@ -76,8 +76,16 @@ def load_workbook(name): return IMPL.load_workbook(name) -def get_workbooks(): - return IMPL.get_workbooks() +def get_workbooks(limit=None, marker=None, sort_keys=None, + sort_dirs=None, fields=None, **kwargs): + return IMPL.get_workbooks( + limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **kwargs + ) def create_workbook(values): @@ -328,8 +336,15 @@ def load_task_execution(name): return IMPL.load_task_execution(name) -def get_task_executions(**kwargs): - return IMPL.get_task_executions(**kwargs) +def get_task_executions(limit=None, marker=None, sort_keys=['created_at'], + sort_dirs=None, **kwargs): + return IMPL.get_task_executions( + limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + **kwargs + ) def create_task_execution(values): @@ -428,8 +443,16 @@ def load_environment(name): return IMPL.load_environment(name) -def get_environments(): - return IMPL.get_environments() +def get_environments(limit=None, marker=None, sort_keys=['name'], + sort_dirs=None, **kwargs): + + return IMPL.get_environments( + limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + **kwargs + ) def create_environment(values): diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index c2cd0466..5cd549e5 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -153,20 +153,56 @@ def _delete_all(model, session=None, **kwargs): _secure_query(model).filter_by(**kwargs).delete(synchronize_session=False) -def _get_collection_sorted_by_name(model, **kwargs): +def _get_collection(model, limit=None, marker=None, sort_keys=None, + sort_dirs=None, fields=None, query=None, **kwargs): + columns = ( + tuple([getattr(model, f) for f in fields if hasattr(model, f)]) + if fields else () + ) + + if query is None: + query = _secure_query(model, *columns).filter_by(**kwargs) + + try: + return _paginate_query( + model, + limit, + marker, + sort_keys, + sort_dirs, + query + ) + except Exception as e: + raise exc.DBQueryEntryException( + "Failed when querying database, error type: %s, " + "error message: %s" % (e.__class__.__name__, e.message) + ) + + +def _get_collection_sorted_by_name(model, fields=None, sort_keys=['name'], + **kwargs): # Note(lane): Sometimes tenant_A needs to get resources of tenant_B, # especially in resource sharing scenario, the resource owner needs to # check if the resource is used by a member. - query = (b.model_query(model) if 'project_id' in kwargs - else _secure_query(model)) + columns = ( + tuple([getattr(model, f) for f in fields if hasattr(model, f)]) + if fields else () + ) - return query.filter_by(**kwargs).order_by(model.name).all() + query = (b.model_query(model, *columns) if 'project_id' in kwargs + else _secure_query(model, *columns)) + + return _get_collection( + model=model, + query=query, + sort_keys=sort_keys, + fields=fields, + **kwargs + ) -def _get_collection_sorted_by_time(model, **kwargs): - query = _secure_query(model) - - return query.filter_by(**kwargs).order_by(model.created_at).all() +def _get_collection_sorted_by_time(model, sort_keys=['created_at'], **kwargs): + return _get_collection(model, sort_keys=sort_keys, **kwargs) def _get_db_object_by_name(model, name): @@ -259,19 +295,6 @@ def delete_workbooks(**kwargs): # Workflow definitions. - -WORKFLOW_COL_MAPPING = { - 'id': models.WorkflowDefinition.id, - 'name': models.WorkflowDefinition.name, - 'input': models.WorkflowDefinition.spec, - 'definition': models.WorkflowDefinition.definition, - 'tags': models.WorkflowDefinition.tags, - 'scope': models.WorkflowDefinition.scope, - 'created_at': models.WorkflowDefinition.created_at, - 'updated_at': models.WorkflowDefinition.updated_at -} - - def get_workflow_definition(identifier): """Gets workflow definition by name or uuid. @@ -306,29 +329,18 @@ def load_workflow_definition(name): return _get_workflow_definition(name) -def get_workflow_definitions(limit=None, marker=None, sort_keys=None, - sort_dirs=None, fields=None, **kwargs): - columns = ( - tuple(WORKFLOW_COL_MAPPING.get(f) for f in fields) if fields else () +def get_workflow_definitions(sort_keys=['created_at'], fields=None, **kwargs): + if fields and 'input' in fields: + fields.remove('input') + fields.append('spec') + + return _get_collection( + model=models.WorkflowDefinition, + sort_keys=sort_keys, + fields=fields, + **kwargs ) - query = _secure_query(models.WorkflowDefinition, *columns) - - try: - return _paginate_query( - models.WorkflowDefinition, - limit, - marker, - sort_keys, - sort_dirs, - query - ) - except Exception as e: - raise exc.DBQueryEntryError( - "Failed when querying database, error type: %s, " - "error message: %s" % (e.__class__.__name__, e.message) - ) - @b.session_aware() def create_workflow_definition(values, session=None): @@ -471,24 +483,12 @@ def load_action_definition(name): return _get_action_definition(name) -def get_action_definitions(limit=None, marker=None, sort_keys=['name'], - sort_dirs=None, **kwargs): - query = _secure_query(models.ActionDefinition).filter_by(**kwargs) - - try: - return _paginate_query( - models.ActionDefinition, - limit, - marker, - sort_keys, - sort_dirs, - query - ) - except Exception as e: - raise exc.DBQueryEntryError( - "Failed when querying database, error type: %s, " - "error message: %s" % (e.__class__.__name__, e.message) - ) +def get_action_definitions(sort_keys=['name'], **kwargs): + return _get_collection( + model=models.ActionDefinition, + sort_keys=sort_keys, + **kwargs + ) @b.session_aware() @@ -747,24 +747,12 @@ def ensure_workflow_execution_exists(id): get_workflow_execution(id) -def get_workflow_executions(limit=None, marker=None, sort_keys=['created_at'], - sort_dirs=None, **kwargs): - query = _secure_query(models.WorkflowExecution).filter_by(**kwargs) - - try: - return _paginate_query( - models.WorkflowExecution, - limit, - marker, - sort_keys, - sort_dirs, - query - ) - except Exception as e: - raise exc.DBQueryEntryError( - "Failed when quering database, error type: %s, " - "error message: %s" % (e.__class__.__name__, e.message) - ) +def get_workflow_executions(sort_keys=['created_at'], **kwargs): + return _get_collection( + models.WorkflowExecution, + sort_keys=sort_keys, + **kwargs + ) @b.session_aware() @@ -1144,10 +1132,15 @@ def _get_cron_trigger(name): return _get_db_object_by_name(models.CronTrigger, name) -def _get_cron_triggers(**kwargs): +def _get_cron_triggers(*columns, **kwargs): query = b.model_query(models.CronTrigger) - return query.filter_by(**kwargs).all() + return _get_collection( + models.CronTrigger, + query=query, + *columns, + **kwargs + ) # Environments. diff --git a/mistral/tests/unit/api/v2/test_action_executions.py b/mistral/tests/unit/api/v2/test_action_executions.py index 259f2771..5ce6e77b 100644 --- a/mistral/tests/unit/api/v2/test_action_executions.py +++ b/mistral/tests/unit/api/v2/test_action_executions.py @@ -185,10 +185,10 @@ class TestActionExecutionsController(base.APITest): self.assertEqual(201, resp.status_int) - action_exec = ACTION_EX + action_exec = copy.deepcopy(ACTION_EX) del action_exec['task_name'] - self.assertDictEqual(ACTION_EX, resp.json) + self.assertDictEqual(action_exec, resp.json) f.assert_called_once_with( ACTION_EX['name'], @@ -212,10 +212,10 @@ class TestActionExecutionsController(base.APITest): self.assertEqual(201, resp.status_int) - action_exec = ACTION_EX + action_exec = copy.deepcopy(ACTION_EX) del action_exec['task_name'] - self.assertDictEqual(ACTION_EX, resp.json) + self.assertDictEqual(action_exec, resp.json) f.assert_called_once_with( ACTION_EX['name'], diff --git a/mistral/utils/rest_utils.py b/mistral/utils/rest_utils.py index b14bf798..cad074e0 100644 --- a/mistral/utils/rest_utils.py +++ b/mistral/utils/rest_utils.py @@ -19,11 +19,15 @@ import json import pecan import six + +from oslo_log import log as logging from webob import Response from wsme import exc as wsme_exc from mistral import exceptions as exc +LOG = logging.getLogger(__name__) + def wrap_wsme_controller_exception(func): """Decorator for controllers method. @@ -102,3 +106,95 @@ def validate_fields(fields, object_fields): raise wsme_exc.ClientSideError( 'Field(s) %s are invalid.' % ', '.join(invalid_fields) ) + + +def get_all(list_cls, cls, get_all_function, get_function, + resource_function=None, marker=None, limit=None, + sort_keys='created_at', sort_dirs='asc', fields='', **filters): + """Return a list of cls. + + :param list_cls: Collection class (e.g.: Actions, Workflows, ...). + :param cls: Class (e.g.: Action, Workflow, ...). + :param get_all_function: Request function to get all elements with + filtering (limit, marker, sort_keys, sort_dirs, + fields) + :param get_function: Function used to fetch the marker + :param resource_function: Optional, function used to fetch additional data + :param marker: Optional. Pagination marker for large data sets. + :param limit: Optional. Maximum number of resources to return in a + single result. Default value is None for backward + compatibility. + :param sort_keys: Optional. Columns to sort results by. + Default: created_at. + :param sort_dirs: Optional. Directions to sort corresponding to + sort_keys, "asc" or "desc" can be choosed. + Default: asc. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's provided, since it will be used when + constructing 'next' link. + :param filters: Optional. A specified dictionary of filters to match. + """ + if fields and 'id' not in fields: + fields.insert(0, 'id') + + validate_query_params(limit, sort_keys, sort_dirs) + validate_fields(fields, cls.get_fields()) + + marker_obj = None + + if marker: + marker_obj = get_function(marker) + + list_to_return = [] + if resource_function: + # do not filter fields yet, resource_function needs the ORM object + db_list = get_all_function( + limit=limit, + marker=marker_obj, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + **filters + ) + + for data in db_list: + obj = resource_function(data) + + # filter fields using a loop instead of the ORM + if fields: + data = [] + for f in fields: + if hasattr(obj, f): + data.append(getattr(obj, f)) + + dict_data = dict(zip(fields, data)) + else: + dict_data = obj.to_dict() + + list_to_return.append(cls.from_dict(dict_data)) + + else: + db_list = get_all_function( + limit=limit, + marker=marker_obj, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + fields=fields, + **filters + ) + + for data in db_list: + dict_data = (dict(zip(fields, data)) if fields else + data.to_dict()) + + list_to_return.append(cls.from_dict(dict_data)) + + return list_cls.convert_with_links( + list_to_return, + limit, + pecan.request.host_url, + sort_keys=','.join(sort_keys), + sort_dirs=','.join(sort_dirs), + fields=','.join(fields) if fields else '', + **filters + )