class ActionExecutionsController(BaseResourceIsolationControllerMixin, ActionExecutionsControllerMixin, ResourceController): """ Implements the RESTful web endpoint that handles the lifecycle of ActionExecutions in the system. """ # Nested controllers views = ExecutionViewsController() children = ActionExecutionChildrenController() attribute = ActionExecutionAttributeController() re_run = ActionExecutionReRunController() # ResourceController attributes query_options = {'sort': ['-start_timestamp', 'action.ref']} supported_filters = SUPPORTED_EXECUTIONS_FILTERS filter_transform_functions = { 'timestamp_gt': lambda value: isotime.parse(value=value), 'timestamp_lt': lambda value: isotime.parse(value=value) } def get_all(self, requester_user, exclude_attributes=None, sort=None, offset=0, limit=None, show_secrets=False, include_attributes=None, advanced_filters=None, **raw_filters): """ List all executions. Handles requests: GET /executions[?exclude_attributes=result,trigger_instance] :param exclude_attributes: List of attributes to exclude from the object. :type exclude_attributes: ``list`` """ # Use a custom sort order when filtering on a timestamp so we return a correct result as # expected by the user query_options = None if raw_filters.get('timestamp_lt', None) or raw_filters.get( 'sort_desc', None): query_options = {'sort': ['-start_timestamp', 'action.ref']} elif raw_filters.get('timestamp_gt', None) or raw_filters.get( 'sort_asc', None): query_options = {'sort': ['+start_timestamp', 'action.ref']} from_model_kwargs = { 'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) } return self._get_action_executions(exclude_fields=exclude_attributes, include_fields=include_attributes, from_model_kwargs=from_model_kwargs, sort=sort, offset=offset, limit=limit, query_options=query_options, raw_filters=raw_filters, advanced_filters=advanced_filters, requester_user=requester_user) def get_one(self, id, requester_user, exclude_attributes=None, show_secrets=False): """ Retrieve a single execution. Handles requests: GET /executions/<id>[?exclude_attributes=result,trigger_instance] :param exclude_attributes: List of attributes to exclude from the object. :type exclude_attributes: ``list`` """ exclude_fields = self._validate_exclude_fields( exclude_fields=exclude_attributes) from_model_kwargs = { 'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) } # Special case for id == "last" if id == 'last': execution_db = ActionExecution.query().order_by('-id').limit( 1).only('id').first() if not execution_db: raise ValueError('No executions found in the database') id = str(execution_db.id) return self._get_one_by_id( id=id, exclude_fields=exclude_fields, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_VIEW) def post(self, liveaction_api, requester_user, context_string=None, show_secrets=False): return self._handle_schedule_execution(liveaction_api=liveaction_api, requester_user=requester_user, context_string=context_string, show_secrets=show_secrets) def put(self, id, liveaction_api, requester_user, show_secrets=False): """ Updates a single execution. Handles requests: PUT /executions/<id> """ if not requester_user: requester_user = UserDB(cfg.CONF.system_user.user) from_model_kwargs = { 'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) } execution_api = self._get_one_by_id( id=id, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_STOP) if not execution_api: abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) liveaction_id = execution_api.liveaction['id'] if not liveaction_id: abort( http_client.INTERNAL_SERVER_ERROR, 'Execution object missing link to liveaction %s.' % liveaction_id) try: liveaction_db = LiveAction.get_by_id(liveaction_id) except: abort( http_client.INTERNAL_SERVER_ERROR, 'Execution object missing link to liveaction %s.' % liveaction_id) if liveaction_db.status in action_constants.LIVEACTION_COMPLETED_STATES: abort(http_client.BAD_REQUEST, 'Execution is already in completed state.') def update_status(liveaction_api, liveaction_db): status = liveaction_api.status result = getattr(liveaction_api, 'result', None) liveaction_db = action_service.update_status( liveaction_db, status, result) actionexecution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) return (liveaction_db, actionexecution_db) try: if (liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELING and liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): if action_service.is_children_active(liveaction_id): liveaction_api.status = action_constants.LIVEACTION_STATUS_CANCELING liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELING or liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): liveaction_db, actionexecution_db = action_service.request_cancellation( liveaction_db, requester_user.name or cfg.CONF.system_user.user) elif (liveaction_db.status == action_constants.LIVEACTION_STATUS_PAUSING and liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): if action_service.is_children_active(liveaction_id): liveaction_api.status = action_constants.LIVEACTION_STATUS_PAUSING liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSING or liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): liveaction_db, actionexecution_db = action_service.request_pause( liveaction_db, requester_user.name or cfg.CONF.system_user.user) elif liveaction_api.status == action_constants.LIVEACTION_STATUS_RESUMING: liveaction_db, actionexecution_db = action_service.request_resume( liveaction_db, requester_user.name or cfg.CONF.system_user.user) else: liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) except runner_exc.InvalidActionRunnerOperationError as e: LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) except runner_exc.UnexpectedActionExecutionStatusError as e: LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) except Exception as e: LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) abort(http_client.INTERNAL_SERVER_ERROR, 'Failed updating execution due to unexpected error.') mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) execution_api = ActionExecutionAPI.from_model( actionexecution_db, mask_secrets=mask_secrets) return execution_api def delete(self, id, requester_user, show_secrets=False): """ Stops a single execution. Handles requests: DELETE /executions/<id> """ if not requester_user: requester_user = UserDB(cfg.CONF.system_user.user) from_model_kwargs = { 'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) } execution_api = self._get_one_by_id( id=id, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_STOP) if not execution_api: abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) liveaction_id = execution_api.liveaction['id'] if not liveaction_id: abort( http_client.INTERNAL_SERVER_ERROR, 'Execution object missing link to liveaction %s.' % liveaction_id) try: liveaction_db = LiveAction.get_by_id(liveaction_id) except: abort( http_client.INTERNAL_SERVER_ERROR, 'Execution object missing link to liveaction %s.' % liveaction_id) if liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELED: LOG.info('Action %s already in "canceled" state; \ returning execution object.' % liveaction_db.id) return execution_api if liveaction_db.status not in action_constants.LIVEACTION_CANCELABLE_STATES: abort( http_client.OK, 'Action cannot be canceled. State = %s.' % liveaction_db.status) try: (liveaction_db, execution_db) = action_service.request_cancellation( liveaction_db, requester_user.name or cfg.CONF.system_user.user) except: LOG.exception('Failed requesting cancellation for liveaction %s.', liveaction_db.id) abort(http_client.INTERNAL_SERVER_ERROR, 'Failed canceling execution.') return ActionExecutionAPI.from_model( execution_db, mask_secrets=from_model_kwargs['mask_secrets']) def _get_action_executions(self, exclude_fields=None, include_fields=None, sort=None, offset=0, limit=None, advanced_filters=None, query_options=None, raw_filters=None, from_model_kwargs=None, requester_user=None): """ :param exclude_fields: A list of object fields to exclude. :type exclude_fields: ``list`` """ if limit is None: limit = self.default_limit limit = int(limit) LOG.debug( 'Retrieving all action executions with filters=%s,exclude_fields=%s,' 'include_fields=%s', raw_filters, exclude_fields, include_fields) return super(ActionExecutionsController, self)._get_all(exclude_fields=exclude_fields, include_fields=include_fields, from_model_kwargs=from_model_kwargs, sort=sort, offset=offset, limit=limit, query_options=query_options, raw_filters=raw_filters, advanced_filters=advanced_filters, requester_user=requester_user)
class ActionExecutionsController( BaseResourceIsolationControllerMixin, ActionExecutionsControllerMixin, ResourceController, ): """ Implements the RESTful web endpoint that handles the lifecycle of ActionExecutions in the system. """ # Nested controllers views = ExecutionViewsController() children = ActionExecutionChildrenController() attribute = ActionExecutionAttributeController() re_run = ActionExecutionReRunController() # ResourceController attributes query_options = {"sort": ["-start_timestamp", "action.ref"]} supported_filters = SUPPORTED_EXECUTIONS_FILTERS filter_transform_functions = { "timestamp_gt": lambda value: isotime.parse(value=value), "timestamp_lt": lambda value: isotime.parse(value=value), } def get_all( self, requester_user, exclude_attributes=None, sort=None, offset=0, limit=None, show_secrets=False, include_attributes=None, advanced_filters=None, **raw_filters, ): """ List all executions. Handles requests: GET /executions[?exclude_attributes=result,trigger_instance] :param exclude_attributes: List of attributes to exclude from the object. :type exclude_attributes: ``list`` """ # Use a custom sort order when filtering on a timestamp so we return a correct result as # expected by the user query_options = None if raw_filters.get("timestamp_lt", None) or raw_filters.get( "sort_desc", None): query_options = {"sort": ["-start_timestamp", "action.ref"]} elif raw_filters.get("timestamp_gt", None) or raw_filters.get( "sort_asc", None): query_options = {"sort": ["+start_timestamp", "action.ref"]} from_model_kwargs = { "mask_secrets": self._get_mask_secrets(requester_user, show_secrets=show_secrets) } return self._get_action_executions( exclude_fields=exclude_attributes, include_fields=include_attributes, from_model_kwargs=from_model_kwargs, sort=sort, offset=offset, limit=limit, query_options=query_options, raw_filters=raw_filters, advanced_filters=advanced_filters, requester_user=requester_user, ) def get_one( self, id, requester_user, exclude_attributes=None, include_attributes=None, show_secrets=False, max_result_size=None, ): """ Retrieve a single execution. Handles requests: GET /executions/<id>[?exclude_attributes=result,trigger_instance] :param exclude_attributes: List of attributes to exclude from the object. :type exclude_attributes: ``list`` """ exclude_fields = self._validate_exclude_fields( exclude_fields=exclude_attributes) include_fields = self._validate_include_fields( include_fields=include_attributes) from_model_kwargs = { "mask_secrets": self._get_mask_secrets(requester_user, show_secrets=show_secrets) } max_result_size = self._validate_max_result_size( max_result_size=max_result_size) # Special case for id == "last" if id == "last": execution_db = (ActionExecution.query().order_by("-id").limit( 1).only("id").first()) if not execution_db: raise ValueError("No executions found in the database") id = str(execution_db.id) return self._get_one_by_id( id=id, exclude_fields=exclude_fields, include_fields=include_fields, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_VIEW, get_by_id_kwargs={"max_result_size": max_result_size}, ) def post(self, liveaction_api, requester_user, context_string=None, show_secrets=False): return self._handle_schedule_execution( liveaction_api=liveaction_api, requester_user=requester_user, context_string=context_string, show_secrets=show_secrets, ) def put(self, id, liveaction_api, requester_user, show_secrets=False): """ Updates a single execution. Handles requests: PUT /executions/<id> """ if not requester_user: requester_user = UserDB(name=cfg.CONF.system_user.user) from_model_kwargs = { "mask_secrets": self._get_mask_secrets(requester_user, show_secrets=show_secrets) } execution_api = self._get_one_by_id( id=id, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_STOP, ) if not execution_api: abort(http_client.NOT_FOUND, "Execution with id %s not found." % id) liveaction_id = execution_api.liveaction["id"] if not liveaction_id: abort( http_client.INTERNAL_SERVER_ERROR, "Execution object missing link to liveaction %s." % liveaction_id, ) try: liveaction_db = LiveAction.get_by_id(liveaction_id) except: abort( http_client.INTERNAL_SERVER_ERROR, "Execution object missing link to liveaction %s." % liveaction_id, ) if liveaction_db.status in action_constants.LIVEACTION_COMPLETED_STATES: abort(http_client.BAD_REQUEST, "Execution is already in completed state.") def update_status(liveaction_api, liveaction_db): status = liveaction_api.status result = getattr(liveaction_api, "result", None) liveaction_db = action_service.update_status(liveaction_db, status, result, set_result_size=True) actionexecution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) return (liveaction_db, actionexecution_db) try: if (liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELING and liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): if action_service.is_children_active(liveaction_id): liveaction_api.status = action_constants.LIVEACTION_STATUS_CANCELING liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELING or liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): liveaction_db, actionexecution_db = action_service.request_cancellation( liveaction_db, requester_user.name or cfg.CONF.system_user.user) elif (liveaction_db.status == action_constants.LIVEACTION_STATUS_PAUSING and liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): if action_service.is_children_active(liveaction_id): liveaction_api.status = action_constants.LIVEACTION_STATUS_PAUSING liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSING or liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): liveaction_db, actionexecution_db = action_service.request_pause( liveaction_db, requester_user.name or cfg.CONF.system_user.user) elif liveaction_api.status == action_constants.LIVEACTION_STATUS_RESUMING: liveaction_db, actionexecution_db = action_service.request_resume( liveaction_db, requester_user.name or cfg.CONF.system_user.user) else: liveaction_db, actionexecution_db = update_status( liveaction_api, liveaction_db) except runner_exc.InvalidActionRunnerOperationError as e: LOG.exception("Failed updating liveaction %s. %s", liveaction_db.id, six.text_type(e)) abort( http_client.BAD_REQUEST, "Failed updating execution. %s" % six.text_type(e), ) except runner_exc.UnexpectedActionExecutionStatusError as e: LOG.exception("Failed updating liveaction %s. %s", liveaction_db.id, six.text_type(e)) abort( http_client.BAD_REQUEST, "Failed updating execution. %s" % six.text_type(e), ) except Exception as e: LOG.exception("Failed updating liveaction %s. %s", liveaction_db.id, six.text_type(e)) abort( http_client.INTERNAL_SERVER_ERROR, "Failed updating execution due to unexpected error.", ) mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) execution_api = ActionExecutionAPI.from_model( actionexecution_db, mask_secrets=mask_secrets) return execution_api def delete(self, id, requester_user, show_secrets=False): """ Stops a single execution. Handles requests: DELETE /executions/<id> """ if not requester_user: requester_user = UserDB(name=cfg.CONF.system_user.user) from_model_kwargs = { "mask_secrets": self._get_mask_secrets(requester_user, show_secrets=show_secrets) } execution_api = self._get_one_by_id( id=id, requester_user=requester_user, from_model_kwargs=from_model_kwargs, permission_type=PermissionType.EXECUTION_STOP, ) if not execution_api: abort(http_client.NOT_FOUND, "Execution with id %s not found." % id) liveaction_id = execution_api.liveaction["id"] if not liveaction_id: abort( http_client.INTERNAL_SERVER_ERROR, "Execution object missing link to liveaction %s." % liveaction_id, ) try: liveaction_db = LiveAction.get_by_id(liveaction_id) except: abort( http_client.INTERNAL_SERVER_ERROR, "Execution object missing link to liveaction %s." % liveaction_id, ) if liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELED: LOG.info('Action %s already in "canceled" state; \ returning execution object.' % liveaction_db.id) return execution_api if liveaction_db.status not in action_constants.LIVEACTION_CANCELABLE_STATES: abort( http_client.OK, "Action cannot be canceled. State = %s." % liveaction_db.status, ) try: (liveaction_db, execution_db) = action_service.request_cancellation( liveaction_db, requester_user.name or cfg.CONF.system_user.user) except: LOG.exception("Failed requesting cancellation for liveaction %s.", liveaction_db.id) abort(http_client.INTERNAL_SERVER_ERROR, "Failed canceling execution.") return ActionExecutionAPI.from_model( execution_db, mask_secrets=from_model_kwargs["mask_secrets"]) def _validate_max_result_size( self, max_result_size: Optional[int]) -> Optional[int]: """ Validate value of the ?max_result_size query parameter (if provided). """ # Maximum limit for MongoDB collection document is 16 MB and the field itself can't be # larger than that obviously. And in reality due to the other fields, overhead, etc, # 14 is the upper limit. if not max_result_size: return max_result_size if max_result_size <= 0: raise ValueError("max_result_size must be a positive number") if max_result_size > 14 * 1024 * 1024: raise ValueError( "max_result_size query parameter must be smaller than 14 MB") return max_result_size def _get_by_id( self, resource_id, exclude_fields=None, include_fields=None, max_result_size=None, ): """ Custom version of _get_by_id() which supports ?max_result_size pre-filtering and not returning result field for executions which result size exceeds this threshold. This functionality allows us to implement fast and efficient retrievals in st2web. """ exclude_fields = exclude_fields or [] include_fields = include_fields or [] if not max_result_size: # If max_result_size is not provided we don't perform any prefiltering and directly # call parent method execution_db = super(ActionExecutionsController, self)._get_by_id( resource_id=resource_id, exclude_fields=exclude_fields, include_fields=include_fields, ) return execution_db # Special query where we check if result size is smaller than pre-defined or that field # doesn't not exist (old executions) and only return the result if the condition is met. # This allows us to implement fast and efficient retrievals of executions on the client # st2web side where we don't want to retrieve and display result directly for executions # with large results # Keep in mind that the query itself is very fast and adds almost no overhead for API # operations which pass this query parameter because we first filter on the ID (indexed # field) and perform projection query with two tiny fields (based on real life testing it # takes less than 3 ms in most scenarios). execution_db = self.access.get( Q(id=resource_id) & (Q(result_size__lte=max_result_size) | Q(result_size__not__exists=True)), only_fields=["id", "result_size"], ) # if result is empty, this means that execution either doesn't exist or the result is # larger than threshold which means we don't want to retrieve and return result to # the end user to we set exclude_fields accordingly if not execution_db: LOG.debug( "Execution with id %s and result_size < %s not found. This means " "execution with this ID doesn't exist or result_size exceeds the " "threshold. Result field will be excluded from the retrieval and " "the response." % (resource_id, max_result_size)) if include_fields and "result" in include_fields: include_fields.remove("result") elif not include_fields: exclude_fields += ["result"] # Now call parent get by id with potentially modified include / exclude fields in case # result should not be included execution_db = super(ActionExecutionsController, self)._get_by_id( resource_id=resource_id, exclude_fields=exclude_fields, include_fields=include_fields, ) return execution_db def _get_action_executions( self, exclude_fields=None, include_fields=None, sort=None, offset=0, limit=None, advanced_filters=None, query_options=None, raw_filters=None, from_model_kwargs=None, requester_user=None, ): """ :param exclude_fields: A list of object fields to exclude. :type exclude_fields: ``list`` """ if limit is None: limit = self.default_limit limit = int(limit) LOG.debug( "Retrieving all action executions with filters=%s,exclude_fields=%s," "include_fields=%s", raw_filters, exclude_fields, include_fields, ) return super(ActionExecutionsController, self)._get_all( exclude_fields=exclude_fields, include_fields=include_fields, from_model_kwargs=from_model_kwargs, sort=sort, offset=offset, limit=limit, query_options=query_options, raw_filters=raw_filters, advanced_filters=advanced_filters, requester_user=requester_user, )