|
1
|
|
|
# Licensed to the StackStorm, Inc ('StackStorm') under one or more |
|
2
|
|
|
# contributor license agreements. See the NOTICE file distributed with |
|
3
|
|
|
# this work for additional information regarding copyright ownership. |
|
4
|
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0 |
|
5
|
|
|
# (the "License"); you may not use this file except in compliance with |
|
6
|
|
|
# the License. You may obtain a copy of the License at |
|
7
|
|
|
# |
|
8
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
9
|
|
|
# |
|
10
|
|
|
# Unless required by applicable law or agreed to in writing, software |
|
11
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
12
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13
|
|
|
# See the License for the specific language governing permissions and |
|
14
|
|
|
# limitations under the License. |
|
15
|
|
|
|
|
16
|
|
|
import copy |
|
17
|
|
|
import re |
|
18
|
|
|
import sys |
|
19
|
|
|
import traceback |
|
20
|
|
|
import itertools |
|
21
|
|
|
|
|
22
|
|
|
import six |
|
23
|
|
|
import jsonschema |
|
24
|
|
|
from oslo_config import cfg |
|
25
|
|
|
from six.moves import http_client |
|
26
|
|
|
|
|
27
|
|
|
from st2api.controllers.base import BaseRestControllerMixin |
|
28
|
|
|
from st2api.controllers.resource import ResourceController |
|
29
|
|
|
from st2api.controllers.v1.executionviews import ExecutionViewsController |
|
30
|
|
|
from st2api.controllers.v1.executionviews import SUPPORTED_FILTERS |
|
31
|
|
|
from st2common import log as logging |
|
32
|
|
|
from st2common.constants import action as action_constants |
|
33
|
|
|
from st2common.exceptions import actionrunner as runner_exc |
|
34
|
|
|
from st2common.exceptions import apivalidation as validation_exc |
|
35
|
|
|
from st2common.exceptions import param as param_exc |
|
36
|
|
|
from st2common.exceptions import trace as trace_exc |
|
37
|
|
|
from st2common.models.api.action import LiveActionAPI |
|
38
|
|
|
from st2common.models.api.action import LiveActionCreateAPI |
|
39
|
|
|
from st2common.models.api.base import cast_argument_value |
|
40
|
|
|
from st2common.models.api.execution import ActionExecutionAPI |
|
41
|
|
|
from st2common.models.api.execution import ActionExecutionOutputAPI |
|
42
|
|
|
from st2common.models.db.auth import UserDB |
|
43
|
|
|
from st2common.persistence.liveaction import LiveAction |
|
44
|
|
|
from st2common.persistence.execution import ActionExecution |
|
45
|
|
|
from st2common.persistence.execution import ActionExecutionOutput |
|
46
|
|
|
from st2common.router import abort |
|
47
|
|
|
from st2common.router import Response |
|
48
|
|
|
from st2common.services import action as action_service |
|
49
|
|
|
from st2common.services import executions as execution_service |
|
50
|
|
|
from st2common.services import trace as trace_service |
|
51
|
|
|
from st2common.services import rbac as rbac_service |
|
52
|
|
|
from st2common.util import isotime |
|
53
|
|
|
from st2common.util import action_db as action_utils |
|
54
|
|
|
from st2common.util import param as param_utils |
|
55
|
|
|
from st2common.util.jsonify import try_loads |
|
56
|
|
|
from st2common.rbac.types import PermissionType |
|
57
|
|
|
from st2common.rbac import utils as rbac_utils |
|
58
|
|
|
from st2common.rbac.utils import assert_user_has_resource_db_permission |
|
59
|
|
|
from st2common.rbac.utils import assert_user_is_admin_if_user_query_param_is_provided |
|
60
|
|
|
from st2common.stream.listener import get_listener |
|
61
|
|
|
|
|
62
|
|
|
__all__ = [ |
|
63
|
|
|
'ActionExecutionsController' |
|
64
|
|
|
] |
|
65
|
|
|
|
|
66
|
|
|
LOG = logging.getLogger(__name__) |
|
67
|
|
|
|
|
68
|
|
|
# Note: We initialize filters here and not in the constructor |
|
69
|
|
|
SUPPORTED_EXECUTIONS_FILTERS = copy.deepcopy(SUPPORTED_FILTERS) |
|
70
|
|
|
SUPPORTED_EXECUTIONS_FILTERS.update({ |
|
71
|
|
|
'timestamp_gt': 'start_timestamp.gt', |
|
72
|
|
|
'timestamp_lt': 'start_timestamp.lt' |
|
73
|
|
|
}) |
|
74
|
|
|
|
|
75
|
|
|
MONITOR_THREAD_EMPTY_Q_SLEEP_TIME = 5 |
|
76
|
|
|
MONITOR_THREAD_NO_WORKERS_SLEEP_TIME = 1 |
|
77
|
|
|
|
|
78
|
|
|
|
|
79
|
|
|
class ActionExecutionsControllerMixin(BaseRestControllerMixin): |
|
80
|
|
|
""" |
|
81
|
|
|
Mixin class with shared methods. |
|
82
|
|
|
""" |
|
83
|
|
|
|
|
84
|
|
|
model = ActionExecutionAPI |
|
85
|
|
|
access = ActionExecution |
|
86
|
|
|
|
|
87
|
|
|
# A list of attributes which can be specified using ?exclude_attributes filter |
|
88
|
|
|
valid_exclude_attributes = [ |
|
89
|
|
|
'result', |
|
90
|
|
|
'trigger_instance' |
|
91
|
|
|
] |
|
92
|
|
|
|
|
93
|
|
|
def _handle_schedule_execution(self, liveaction_api, requester_user, context_string=None, |
|
94
|
|
|
show_secrets=False): |
|
95
|
|
|
""" |
|
96
|
|
|
:param liveaction: LiveActionAPI object. |
|
97
|
|
|
:type liveaction: :class:`LiveActionAPI` |
|
98
|
|
|
""" |
|
99
|
|
|
|
|
100
|
|
|
if not requester_user: |
|
101
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
|
102
|
|
|
|
|
103
|
|
|
# Assert action ref is valid |
|
104
|
|
|
action_ref = liveaction_api.action |
|
105
|
|
|
action_db = action_utils.get_action_by_ref(action_ref) |
|
106
|
|
|
|
|
107
|
|
|
if not action_db: |
|
108
|
|
|
message = 'Action "%s" cannot be found.' % action_ref |
|
109
|
|
|
LOG.warning(message) |
|
110
|
|
|
abort(http_client.BAD_REQUEST, message) |
|
111
|
|
|
|
|
112
|
|
|
# Assert the permissions |
|
113
|
|
|
assert_user_has_resource_db_permission(user_db=requester_user, resource_db=action_db, |
|
114
|
|
|
permission_type=PermissionType.ACTION_EXECUTE) |
|
115
|
|
|
|
|
116
|
|
|
# Validate that the authenticated user is admin if user query param is provided |
|
117
|
|
|
user = liveaction_api.user or requester_user.name |
|
118
|
|
|
assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user, |
|
119
|
|
|
user=user) |
|
120
|
|
|
|
|
121
|
|
|
try: |
|
122
|
|
|
return self._schedule_execution(liveaction=liveaction_api, |
|
123
|
|
|
requester_user=requester_user, |
|
124
|
|
|
user=user, |
|
125
|
|
|
context_string=context_string, |
|
126
|
|
|
show_secrets=show_secrets, |
|
127
|
|
|
pack=action_db.pack) |
|
128
|
|
|
except ValueError as e: |
|
129
|
|
|
LOG.exception('Unable to execute action.') |
|
130
|
|
|
abort(http_client.BAD_REQUEST, str(e)) |
|
131
|
|
|
except jsonschema.ValidationError as e: |
|
132
|
|
|
LOG.exception('Unable to execute action. Parameter validation failed.') |
|
133
|
|
|
abort(http_client.BAD_REQUEST, re.sub("u'([^']*)'", r"'\1'", e.message)) |
|
134
|
|
|
except trace_exc.TraceNotFoundException as e: |
|
135
|
|
|
abort(http_client.BAD_REQUEST, str(e)) |
|
136
|
|
|
except validation_exc.ValueValidationException as e: |
|
137
|
|
|
raise e |
|
138
|
|
|
except Exception as e: |
|
139
|
|
|
LOG.exception('Unable to execute action. Unexpected error encountered.') |
|
140
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, str(e)) |
|
141
|
|
|
|
|
142
|
|
|
def _schedule_execution(self, |
|
143
|
|
|
liveaction, |
|
144
|
|
|
requester_user, |
|
145
|
|
|
user=None, |
|
146
|
|
|
context_string=None, |
|
147
|
|
|
show_secrets=False, |
|
148
|
|
|
pack=None): |
|
149
|
|
|
# Initialize execution context if it does not exist. |
|
150
|
|
|
if not hasattr(liveaction, 'context'): |
|
151
|
|
|
liveaction.context = dict() |
|
152
|
|
|
|
|
153
|
|
|
liveaction.context['user'] = user |
|
154
|
|
|
liveaction.context['pack'] = pack |
|
155
|
|
|
LOG.debug('User is: %s' % liveaction.context['user']) |
|
156
|
|
|
|
|
157
|
|
|
# Retrieve other st2 context from request header. |
|
158
|
|
|
if context_string: |
|
159
|
|
|
context = try_loads(context_string) |
|
160
|
|
|
if not isinstance(context, dict): |
|
161
|
|
|
raise ValueError('Unable to convert st2-context from the headers into JSON.') |
|
162
|
|
|
liveaction.context.update(context) |
|
163
|
|
|
|
|
164
|
|
|
# Include RBAC context (if RBAC is available and enabled) |
|
165
|
|
|
if cfg.CONF.rbac.enable: |
|
166
|
|
|
user_db = UserDB(name=user) |
|
167
|
|
|
role_dbs = rbac_service.get_roles_for_user(user_db=user_db, include_remote=True) |
|
168
|
|
|
roles = [role_db.name for role_db in role_dbs] |
|
169
|
|
|
liveaction.context['rbac'] = { |
|
170
|
|
|
'user': user, |
|
171
|
|
|
'roles': roles |
|
172
|
|
|
} |
|
173
|
|
|
|
|
174
|
|
|
# Schedule the action execution. |
|
175
|
|
|
liveaction_db = LiveActionAPI.to_model(liveaction) |
|
176
|
|
|
action_db = action_utils.get_action_by_ref(liveaction_db.action) |
|
177
|
|
|
runnertype_db = action_utils.get_runnertype_by_name(action_db.runner_type['name']) |
|
178
|
|
|
|
|
179
|
|
|
try: |
|
180
|
|
|
liveaction_db.parameters = param_utils.render_live_params( |
|
181
|
|
|
runnertype_db.runner_parameters, action_db.parameters, liveaction_db.parameters, |
|
182
|
|
|
liveaction_db.context) |
|
183
|
|
|
except param_exc.ParamException: |
|
184
|
|
|
|
|
185
|
|
|
# We still need to create a request, so liveaction_db is assigned an ID |
|
186
|
|
|
liveaction_db, actionexecution_db = action_service.create_request(liveaction_db) |
|
187
|
|
|
|
|
188
|
|
|
# By this point the execution is already in the DB therefore need to mark it failed. |
|
189
|
|
|
_, e, tb = sys.exc_info() |
|
190
|
|
|
action_service.update_status( |
|
191
|
|
|
liveaction=liveaction_db, |
|
192
|
|
|
new_status=action_constants.LIVEACTION_STATUS_FAILED, |
|
193
|
|
|
result={'error': str(e), 'traceback': ''.join(traceback.format_tb(tb, 20))}) |
|
194
|
|
|
# Might be a good idea to return the actual ActionExecution rather than bubble up |
|
195
|
|
|
# the exception. |
|
196
|
|
|
raise validation_exc.ValueValidationException(str(e)) |
|
197
|
|
|
|
|
198
|
|
|
# The request should be created after the above call to render_live_params |
|
199
|
|
|
# so any templates in live parameters have a chance to render. |
|
200
|
|
|
liveaction_db, actionexecution_db = action_service.create_request(liveaction_db) |
|
201
|
|
|
liveaction_db = LiveAction.add_or_update(liveaction_db, publish=False) |
|
202
|
|
|
|
|
203
|
|
|
_, actionexecution_db = action_service.publish_request(liveaction_db, actionexecution_db) |
|
204
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
205
|
|
|
execution_api = ActionExecutionAPI.from_model(actionexecution_db, mask_secrets=mask_secrets) |
|
206
|
|
|
|
|
207
|
|
|
return Response(json=execution_api, status=http_client.CREATED) |
|
208
|
|
|
|
|
209
|
|
|
def _get_result_object(self, id): |
|
210
|
|
|
""" |
|
211
|
|
|
Retrieve result object for the provided action execution. |
|
212
|
|
|
|
|
213
|
|
|
:param id: Action execution ID. |
|
214
|
|
|
:type id: ``str`` |
|
215
|
|
|
|
|
216
|
|
|
:rtype: ``dict`` |
|
217
|
|
|
""" |
|
218
|
|
|
fields = ['result'] |
|
219
|
|
|
action_exec_db = self.access.impl.model.objects.filter(id=id).only(*fields).get() |
|
220
|
|
|
return action_exec_db.result |
|
221
|
|
|
|
|
222
|
|
|
def _get_children(self, id_, requester_user, depth=-1, result_fmt=None, |
|
223
|
|
|
show_secrets=False): |
|
224
|
|
|
# make sure depth is int. Url encoding will make it a string and needs to |
|
225
|
|
|
# be converted back in that case. |
|
226
|
|
|
depth = int(depth) |
|
227
|
|
|
LOG.debug('retrieving children for id: %s with depth: %s', id_, depth) |
|
228
|
|
|
descendants = execution_service.get_descendants(actionexecution_id=id_, |
|
229
|
|
|
descendant_depth=depth, |
|
230
|
|
|
result_fmt=result_fmt) |
|
231
|
|
|
|
|
232
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
233
|
|
|
return [self.model.from_model(descendant, mask_secrets=mask_secrets) for |
|
234
|
|
|
descendant in descendants] |
|
235
|
|
|
|
|
236
|
|
|
|
|
237
|
|
|
class BaseActionExecutionNestedController(ActionExecutionsControllerMixin, ResourceController): |
|
238
|
|
|
# Note: We need to override "get_one" and "get_all" to return 404 since nested controller |
|
239
|
|
|
# don't implement thos methods |
|
240
|
|
|
|
|
241
|
|
|
# ResourceController attributes |
|
242
|
|
|
query_options = {} |
|
243
|
|
|
supported_filters = {} |
|
244
|
|
|
|
|
245
|
|
|
def get_all(self): |
|
246
|
|
|
abort(http_client.NOT_FOUND) |
|
247
|
|
|
|
|
248
|
|
|
def get_one(self, id): |
|
249
|
|
|
abort(http_client.NOT_FOUND) |
|
250
|
|
|
|
|
251
|
|
|
|
|
252
|
|
|
class ActionExecutionChildrenController(BaseActionExecutionNestedController): |
|
253
|
|
|
def get_one(self, id, requester_user, depth=-1, result_fmt=None, show_secrets=False): |
|
254
|
|
|
""" |
|
255
|
|
|
Retrieve children for the provided action execution. |
|
256
|
|
|
|
|
257
|
|
|
:rtype: ``list`` |
|
258
|
|
|
""" |
|
259
|
|
|
|
|
260
|
|
|
instance = self._get_by_id(resource_id=id) |
|
261
|
|
|
|
|
262
|
|
|
permission_type = PermissionType.EXECUTION_VIEW |
|
263
|
|
|
rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user, |
|
264
|
|
|
resource_db=instance, |
|
265
|
|
|
permission_type=permission_type) |
|
266
|
|
|
|
|
267
|
|
|
return self._get_children(id_=id, depth=depth, result_fmt=result_fmt, |
|
268
|
|
|
requester_user=requester_user, show_secrets=show_secrets) |
|
269
|
|
|
|
|
270
|
|
|
|
|
271
|
|
|
class ActionExecutionAttributeController(BaseActionExecutionNestedController): |
|
272
|
|
|
valid_exclude_attributes = ['action__pack', 'action__uid'] + \ |
|
273
|
|
|
ActionExecutionsControllerMixin.valid_exclude_attributes |
|
274
|
|
|
|
|
275
|
|
|
def get(self, id, attribute, requester_user): |
|
276
|
|
|
""" |
|
277
|
|
|
Retrieve a particular attribute for the provided action execution. |
|
278
|
|
|
|
|
279
|
|
|
Handles requests: |
|
280
|
|
|
|
|
281
|
|
|
GET /executions/<id>/attribute/<attribute name> |
|
282
|
|
|
|
|
283
|
|
|
:rtype: ``dict`` |
|
284
|
|
|
""" |
|
285
|
|
|
fields = [attribute, 'action__pack', 'action__uid'] |
|
286
|
|
|
fields = self._validate_exclude_fields(fields) |
|
287
|
|
|
action_exec_db = self.access.impl.model.objects.filter(id=id).only(*fields).get() |
|
288
|
|
|
|
|
289
|
|
|
permission_type = PermissionType.EXECUTION_VIEW |
|
290
|
|
|
rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user, |
|
291
|
|
|
resource_db=action_exec_db, |
|
292
|
|
|
permission_type=permission_type) |
|
293
|
|
|
|
|
294
|
|
|
result = getattr(action_exec_db, attribute, None) |
|
295
|
|
|
return result |
|
296
|
|
|
|
|
297
|
|
|
|
|
298
|
|
|
class ActionExecutionOutputController(ActionExecutionsControllerMixin, ResourceController): |
|
299
|
|
|
supported_filters = { |
|
300
|
|
|
'output_type': 'output_type' |
|
301
|
|
|
} |
|
302
|
|
|
exclude_fields = [] |
|
303
|
|
|
|
|
304
|
|
|
CLOSE_STREAM_LIVEACTION_STATES = action_constants.LIVEACTION_COMPLETED_STATES + [ |
|
305
|
|
|
action_constants.LIVEACTION_STATUS_PAUSING, |
|
306
|
|
|
action_constants.LIVEACTION_STATUS_RESUMING |
|
307
|
|
|
] |
|
308
|
|
|
|
|
309
|
|
|
def get_one(self, id, output_type=None, requester_user=None): |
|
310
|
|
|
# Special case for id == "last" |
|
311
|
|
|
if id == 'last': |
|
312
|
|
|
execution_db = ActionExecution.query().order_by('-id').limit(1).first() |
|
313
|
|
|
else: |
|
314
|
|
|
execution_db = self._get_one_by_id(id=id, requester_user=requester_user, |
|
315
|
|
|
permission_type=PermissionType.EXECUTION_VIEW) |
|
316
|
|
|
|
|
317
|
|
|
execution_id = str(execution_db.id) |
|
318
|
|
|
|
|
319
|
|
|
query_filters = {} |
|
320
|
|
|
if output_type and output_type != 'all': |
|
321
|
|
|
query_filters['output_type'] = output_type |
|
322
|
|
|
|
|
323
|
|
|
def existing_output_iter(): |
|
324
|
|
|
# Consume and return all of the existing lines |
|
325
|
|
|
# pylint: disable=no-member |
|
326
|
|
|
output_dbs = ActionExecutionOutput.query(execution_id=execution_id, **query_filters) |
|
327
|
|
|
|
|
328
|
|
|
# Note: We return all at once instead of yield line by line to avoid multiple socket |
|
329
|
|
|
# writes and to achieve better performance |
|
330
|
|
|
output = ''.join([output_db.data for output_db in output_dbs]) |
|
331
|
|
|
yield six.binary_type(output.encode('utf-8')) |
|
332
|
|
|
|
|
333
|
|
|
def new_output_iter(): |
|
334
|
|
|
def noop_gen(): |
|
335
|
|
|
yield six.binary_type('') |
|
336
|
|
|
|
|
337
|
|
|
# Bail out if execution has already completed / been paused |
|
338
|
|
|
if execution_db.status in self.CLOSE_STREAM_LIVEACTION_STATES: |
|
339
|
|
|
return noop_gen() |
|
340
|
|
|
|
|
341
|
|
|
# Wait for and return any new line which may come in |
|
342
|
|
|
execution_ids = [execution_id] |
|
343
|
|
|
listener = get_listener(name='execution_output') # pylint: disable=no-member |
|
344
|
|
|
gen = listener.generator(execution_ids=execution_ids) |
|
345
|
|
|
|
|
346
|
|
|
def format(gen): |
|
347
|
|
|
for pack in gen: |
|
348
|
|
|
if not pack: |
|
349
|
|
|
continue |
|
350
|
|
|
else: |
|
351
|
|
|
(_, model_api) = pack |
|
352
|
|
|
|
|
353
|
|
|
# Note: gunicorn wsgi handler expect bytes, not unicode |
|
354
|
|
|
# pylint: disable=no-member |
|
355
|
|
|
if isinstance(model_api, ActionExecutionOutputAPI): |
|
356
|
|
|
if output_type and model_api.output_type != output_type: |
|
357
|
|
|
continue |
|
358
|
|
|
|
|
359
|
|
|
yield six.binary_type(model_api.data.encode('utf-8')) |
|
360
|
|
|
elif isinstance(model_api, ActionExecutionAPI): |
|
361
|
|
|
if model_api.status in self.CLOSE_STREAM_LIVEACTION_STATES: |
|
362
|
|
|
yield six.binary_type('') |
|
363
|
|
|
break |
|
364
|
|
|
else: |
|
365
|
|
|
LOG.debug('Unrecognized message type: %s' % (model_api)) |
|
366
|
|
|
|
|
367
|
|
|
gen = format(gen) |
|
368
|
|
|
return gen |
|
369
|
|
|
|
|
370
|
|
|
def make_response(): |
|
371
|
|
|
app_iter = itertools.chain(existing_output_iter(), new_output_iter()) |
|
372
|
|
|
res = Response(content_type='text/plain', app_iter=app_iter) |
|
373
|
|
|
return res |
|
374
|
|
|
|
|
375
|
|
|
res = make_response() |
|
376
|
|
|
return res |
|
377
|
|
|
|
|
378
|
|
|
|
|
379
|
|
|
class ActionExecutionReRunController(ActionExecutionsControllerMixin, ResourceController): |
|
380
|
|
|
supported_filters = {} |
|
381
|
|
|
exclude_fields = [ |
|
382
|
|
|
'result', |
|
383
|
|
|
'trigger_instance' |
|
384
|
|
|
] |
|
385
|
|
|
|
|
386
|
|
|
class ExecutionSpecificationAPI(object): |
|
387
|
|
|
def __init__(self, parameters=None, tasks=None, reset=None, user=None): |
|
388
|
|
|
self.parameters = parameters or {} |
|
389
|
|
|
self.tasks = tasks or [] |
|
390
|
|
|
self.reset = reset or [] |
|
391
|
|
|
self.user = user |
|
392
|
|
|
|
|
393
|
|
|
def validate(self): |
|
394
|
|
|
if (self.tasks or self.reset) and self.parameters: |
|
395
|
|
|
raise ValueError('Parameters override is not supported when ' |
|
396
|
|
|
're-running task(s) for a workflow.') |
|
397
|
|
|
|
|
398
|
|
|
if self.parameters: |
|
399
|
|
|
assert isinstance(self.parameters, dict) |
|
400
|
|
|
|
|
401
|
|
|
if self.tasks: |
|
402
|
|
|
assert isinstance(self.tasks, list) |
|
403
|
|
|
|
|
404
|
|
|
if self.reset: |
|
405
|
|
|
assert isinstance(self.reset, list) |
|
406
|
|
|
|
|
407
|
|
|
if list(set(self.reset) - set(self.tasks)): |
|
408
|
|
|
raise ValueError('List of tasks to reset does not match the tasks to rerun.') |
|
409
|
|
|
|
|
410
|
|
|
return self |
|
411
|
|
|
|
|
412
|
|
|
def post(self, spec_api, id, requester_user, no_merge=False, show_secrets=False): |
|
413
|
|
|
""" |
|
414
|
|
|
Re-run the provided action execution optionally specifying override parameters. |
|
415
|
|
|
|
|
416
|
|
|
Handles requests: |
|
417
|
|
|
|
|
418
|
|
|
POST /executions/<id>/re_run |
|
419
|
|
|
""" |
|
420
|
|
|
|
|
421
|
|
|
if (spec_api.tasks or spec_api.reset) and spec_api.parameters: |
|
422
|
|
|
raise ValueError('Parameters override is not supported when ' |
|
423
|
|
|
're-running task(s) for a workflow.') |
|
424
|
|
|
|
|
425
|
|
|
if spec_api.parameters: |
|
426
|
|
|
assert isinstance(spec_api.parameters, dict) |
|
427
|
|
|
|
|
428
|
|
|
if spec_api.tasks: |
|
429
|
|
|
assert isinstance(spec_api.tasks, list) |
|
430
|
|
|
|
|
431
|
|
|
if spec_api.reset: |
|
432
|
|
|
assert isinstance(spec_api.reset, list) |
|
433
|
|
|
|
|
434
|
|
|
if list(set(spec_api.reset) - set(spec_api.tasks)): |
|
435
|
|
|
raise ValueError('List of tasks to reset does not match the tasks to rerun.') |
|
436
|
|
|
|
|
437
|
|
|
no_merge = cast_argument_value(value_type=bool, value=no_merge) |
|
438
|
|
|
existing_execution = self._get_one_by_id(id=id, exclude_fields=self.exclude_fields, |
|
439
|
|
|
requester_user=requester_user, |
|
440
|
|
|
permission_type=PermissionType.EXECUTION_VIEW) |
|
441
|
|
|
|
|
442
|
|
|
if spec_api.tasks and existing_execution.runner['name'] != 'mistral-v2': |
|
443
|
|
|
raise ValueError('Task option is only supported for Mistral workflows.') |
|
444
|
|
|
|
|
445
|
|
|
# Merge in any parameters provided by the user |
|
446
|
|
|
new_parameters = {} |
|
447
|
|
|
if not no_merge: |
|
448
|
|
|
new_parameters.update(getattr(existing_execution, 'parameters', {})) |
|
449
|
|
|
new_parameters.update(spec_api.parameters) |
|
450
|
|
|
|
|
451
|
|
|
# Create object for the new execution |
|
452
|
|
|
action_ref = existing_execution.action['ref'] |
|
453
|
|
|
|
|
454
|
|
|
# Include additional option(s) for the execution |
|
455
|
|
|
context = { |
|
456
|
|
|
're-run': { |
|
457
|
|
|
'ref': id, |
|
458
|
|
|
} |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
if spec_api.tasks: |
|
462
|
|
|
context['re-run']['tasks'] = spec_api.tasks |
|
463
|
|
|
|
|
464
|
|
|
if spec_api.reset: |
|
465
|
|
|
context['re-run']['reset'] = spec_api.reset |
|
466
|
|
|
|
|
467
|
|
|
# Add trace to the new execution |
|
468
|
|
|
trace = trace_service.get_trace_db_by_action_execution( |
|
469
|
|
|
action_execution_id=existing_execution.id) |
|
470
|
|
|
|
|
471
|
|
|
if trace: |
|
472
|
|
|
context['trace_context'] = {'id_': str(trace.id)} |
|
473
|
|
|
|
|
474
|
|
|
new_liveaction_api = LiveActionCreateAPI(action=action_ref, |
|
475
|
|
|
context=context, |
|
476
|
|
|
parameters=new_parameters, |
|
477
|
|
|
user=spec_api.user) |
|
478
|
|
|
|
|
479
|
|
|
return self._handle_schedule_execution(liveaction_api=new_liveaction_api, |
|
480
|
|
|
requester_user=requester_user, |
|
481
|
|
|
show_secrets=show_secrets) |
|
482
|
|
|
|
|
483
|
|
|
|
|
484
|
|
|
class ActionExecutionsController(ActionExecutionsControllerMixin, ResourceController): |
|
485
|
|
|
""" |
|
486
|
|
|
Implements the RESTful web endpoint that handles |
|
487
|
|
|
the lifecycle of ActionExecutions in the system. |
|
488
|
|
|
""" |
|
489
|
|
|
|
|
490
|
|
|
# Nested controllers |
|
491
|
|
|
views = ExecutionViewsController() |
|
492
|
|
|
|
|
493
|
|
|
children = ActionExecutionChildrenController() |
|
494
|
|
|
attribute = ActionExecutionAttributeController() |
|
495
|
|
|
re_run = ActionExecutionReRunController() |
|
496
|
|
|
|
|
497
|
|
|
# ResourceController attributes |
|
498
|
|
|
query_options = { |
|
499
|
|
|
'sort': ['-start_timestamp', 'action.ref'] |
|
500
|
|
|
} |
|
501
|
|
|
supported_filters = SUPPORTED_EXECUTIONS_FILTERS |
|
502
|
|
|
filter_transform_functions = { |
|
503
|
|
|
'timestamp_gt': lambda value: isotime.parse(value=value), |
|
504
|
|
|
'timestamp_lt': lambda value: isotime.parse(value=value) |
|
505
|
|
|
} |
|
506
|
|
|
|
|
507
|
|
|
def get_all(self, requester_user, exclude_attributes=None, sort=None, offset=0, limit=None, |
|
508
|
|
|
show_secrets=False, include_attributes=None, **raw_filters): |
|
509
|
|
|
""" |
|
510
|
|
|
List all executions. |
|
511
|
|
|
|
|
512
|
|
|
Handles requests: |
|
513
|
|
|
GET /executions[?exclude_attributes=result,trigger_instance] |
|
514
|
|
|
|
|
515
|
|
|
:param exclude_attributes: List of attributes to exclude from the object. |
|
516
|
|
|
:type exclude_attributes: ``list`` |
|
517
|
|
|
""" |
|
518
|
|
|
exclude_fields = self._validate_exclude_fields(exclude_fields=exclude_attributes) |
|
519
|
|
|
|
|
520
|
|
|
# Use a custom sort order when filtering on a timestamp so we return a correct result as |
|
521
|
|
|
# expected by the user |
|
522
|
|
|
query_options = None |
|
523
|
|
|
if raw_filters.get('timestamp_lt', None) or raw_filters.get('sort_desc', None): |
|
524
|
|
|
query_options = {'sort': ['-start_timestamp', 'action.ref']} |
|
525
|
|
|
elif raw_filters.get('timestamp_gt', None) or raw_filters.get('sort_asc', None): |
|
526
|
|
|
query_options = {'sort': ['+start_timestamp', 'action.ref']} |
|
527
|
|
|
|
|
528
|
|
|
from_model_kwargs = { |
|
529
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
530
|
|
|
} |
|
531
|
|
|
return self._get_action_executions(exclude_fields=exclude_fields, |
|
532
|
|
|
include_fields=include_attributes, |
|
533
|
|
|
from_model_kwargs=from_model_kwargs, |
|
534
|
|
|
sort=sort, |
|
535
|
|
|
offset=offset, |
|
536
|
|
|
limit=limit, |
|
537
|
|
|
query_options=query_options, |
|
538
|
|
|
raw_filters=raw_filters, |
|
539
|
|
|
requester_user=requester_user) |
|
540
|
|
|
|
|
541
|
|
|
def get_one(self, id, requester_user, exclude_attributes=None, show_secrets=False): |
|
542
|
|
|
""" |
|
543
|
|
|
Retrieve a single execution. |
|
544
|
|
|
|
|
545
|
|
|
Handles requests: |
|
546
|
|
|
GET /executions/<id>[?exclude_attributes=result,trigger_instance] |
|
547
|
|
|
|
|
548
|
|
|
:param exclude_attributes: List of attributes to exclude from the object. |
|
549
|
|
|
:type exclude_attributes: ``list`` |
|
550
|
|
|
""" |
|
551
|
|
|
exclude_fields = self._validate_exclude_fields(exclude_fields=exclude_attributes) |
|
552
|
|
|
|
|
553
|
|
|
from_model_kwargs = { |
|
554
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
555
|
|
|
} |
|
556
|
|
|
|
|
557
|
|
|
# Special case for id == "last" |
|
558
|
|
|
if id == 'last': |
|
559
|
|
|
execution_db = ActionExecution.query().order_by('-id').limit(1).only('id').first() |
|
560
|
|
|
id = str(execution_db.id) |
|
561
|
|
|
|
|
562
|
|
|
return self._get_one_by_id(id=id, exclude_fields=exclude_fields, |
|
563
|
|
|
requester_user=requester_user, |
|
564
|
|
|
from_model_kwargs=from_model_kwargs, |
|
565
|
|
|
permission_type=PermissionType.EXECUTION_VIEW) |
|
566
|
|
|
|
|
567
|
|
|
def post(self, liveaction_api, requester_user, context_string=None, show_secrets=False): |
|
568
|
|
|
return self._handle_schedule_execution(liveaction_api=liveaction_api, |
|
569
|
|
|
requester_user=requester_user, |
|
570
|
|
|
context_string=context_string, |
|
571
|
|
|
show_secrets=show_secrets) |
|
572
|
|
|
|
|
573
|
|
|
def put(self, id, liveaction_api, requester_user, show_secrets=False): |
|
574
|
|
|
""" |
|
575
|
|
|
Updates a single execution. |
|
576
|
|
|
|
|
577
|
|
|
Handles requests: |
|
578
|
|
|
PUT /executions/<id> |
|
579
|
|
|
|
|
580
|
|
|
""" |
|
581
|
|
|
if not requester_user: |
|
582
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
|
583
|
|
|
|
|
584
|
|
|
from_model_kwargs = { |
|
585
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
586
|
|
|
} |
|
587
|
|
|
|
|
588
|
|
|
execution_api = self._get_one_by_id(id=id, requester_user=requester_user, |
|
589
|
|
|
from_model_kwargs=from_model_kwargs, |
|
590
|
|
|
permission_type=PermissionType.EXECUTION_STOP) |
|
591
|
|
|
|
|
592
|
|
|
if not execution_api: |
|
593
|
|
|
abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) |
|
594
|
|
|
|
|
595
|
|
|
liveaction_id = execution_api.liveaction['id'] |
|
596
|
|
|
if not liveaction_id: |
|
597
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
|
598
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
|
599
|
|
|
|
|
600
|
|
|
try: |
|
601
|
|
|
liveaction_db = LiveAction.get_by_id(liveaction_id) |
|
602
|
|
|
except: |
|
603
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
|
604
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
|
605
|
|
|
|
|
606
|
|
|
if liveaction_db.status in action_constants.LIVEACTION_COMPLETED_STATES: |
|
607
|
|
|
abort(http_client.BAD_REQUEST, 'Execution is already in completed state.') |
|
608
|
|
|
|
|
609
|
|
|
def update_status(liveaction_api, liveaction_db): |
|
610
|
|
|
status = liveaction_api.status |
|
611
|
|
|
result = getattr(liveaction_api, 'result', None) |
|
612
|
|
|
liveaction_db = action_service.update_status(liveaction_db, status, result) |
|
613
|
|
|
actionexecution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) |
|
614
|
|
|
return (liveaction_db, actionexecution_db) |
|
615
|
|
|
|
|
616
|
|
|
try: |
|
617
|
|
|
if (liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELING and |
|
618
|
|
|
liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): |
|
619
|
|
|
if action_service.is_children_active(liveaction_id): |
|
620
|
|
|
liveaction_api.status = action_constants.LIVEACTION_STATUS_CANCELING |
|
621
|
|
|
liveaction_db, actionexecution_db = update_status(liveaction_api, liveaction_db) |
|
622
|
|
|
elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELING or |
|
623
|
|
|
liveaction_api.status == action_constants.LIVEACTION_STATUS_CANCELED): |
|
624
|
|
|
liveaction_db, actionexecution_db = action_service.request_cancellation( |
|
625
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
|
626
|
|
|
elif (liveaction_db.status == action_constants.LIVEACTION_STATUS_PAUSING and |
|
627
|
|
|
liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): |
|
628
|
|
|
if action_service.is_children_active(liveaction_id): |
|
629
|
|
|
liveaction_api.status = action_constants.LIVEACTION_STATUS_PAUSING |
|
630
|
|
|
liveaction_db, actionexecution_db = update_status(liveaction_api, liveaction_db) |
|
631
|
|
|
elif (liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSING or |
|
632
|
|
|
liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): |
|
633
|
|
|
liveaction_db, actionexecution_db = action_service.request_pause( |
|
634
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
|
635
|
|
|
elif liveaction_api.status == action_constants.LIVEACTION_STATUS_RESUMING: |
|
636
|
|
|
liveaction_db, actionexecution_db = action_service.request_resume( |
|
637
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
|
638
|
|
|
else: |
|
639
|
|
|
liveaction_db, actionexecution_db = update_status(liveaction_api, liveaction_db) |
|
640
|
|
|
except runner_exc.InvalidActionRunnerOperationError as e: |
|
641
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
|
642
|
|
|
abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) |
|
643
|
|
|
except runner_exc.UnexpectedActionExecutionStatusError as e: |
|
644
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
|
645
|
|
|
abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) |
|
646
|
|
|
except Exception as e: |
|
647
|
|
|
print str(e) |
|
648
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
|
649
|
|
|
abort( |
|
650
|
|
|
http_client.INTERNAL_SERVER_ERROR, |
|
651
|
|
|
'Failed updating execution due to unexpected error.' |
|
652
|
|
|
) |
|
653
|
|
|
|
|
654
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
655
|
|
|
execution_api = ActionExecutionAPI.from_model(actionexecution_db, mask_secrets=mask_secrets) |
|
656
|
|
|
|
|
657
|
|
|
return execution_api |
|
658
|
|
|
|
|
659
|
|
|
def delete(self, id, requester_user, show_secrets=False): |
|
660
|
|
|
""" |
|
661
|
|
|
Stops a single execution. |
|
662
|
|
|
|
|
663
|
|
|
Handles requests: |
|
664
|
|
|
DELETE /executions/<id> |
|
665
|
|
|
|
|
666
|
|
|
""" |
|
667
|
|
|
if not requester_user: |
|
668
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
|
669
|
|
|
|
|
670
|
|
|
from_model_kwargs = { |
|
671
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
|
672
|
|
|
} |
|
673
|
|
|
execution_api = self._get_one_by_id(id=id, requester_user=requester_user, |
|
674
|
|
|
from_model_kwargs=from_model_kwargs, |
|
675
|
|
|
permission_type=PermissionType.EXECUTION_STOP) |
|
676
|
|
|
|
|
677
|
|
|
if not execution_api: |
|
678
|
|
|
abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) |
|
679
|
|
|
|
|
680
|
|
|
liveaction_id = execution_api.liveaction['id'] |
|
681
|
|
|
if not liveaction_id: |
|
682
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
|
683
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
|
684
|
|
|
|
|
685
|
|
|
try: |
|
686
|
|
|
liveaction_db = LiveAction.get_by_id(liveaction_id) |
|
687
|
|
|
except: |
|
688
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
|
689
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
|
690
|
|
|
|
|
691
|
|
|
if liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELED: |
|
692
|
|
|
LOG.info( |
|
693
|
|
|
'Action %s already in "canceled" state; \ |
|
694
|
|
|
returning execution object.' % liveaction_db.id |
|
695
|
|
|
) |
|
696
|
|
|
return execution_api |
|
697
|
|
|
|
|
698
|
|
|
if liveaction_db.status not in action_constants.LIVEACTION_CANCELABLE_STATES: |
|
699
|
|
|
abort(http_client.OK, 'Action cannot be canceled. State = %s.' % liveaction_db.status) |
|
700
|
|
|
|
|
701
|
|
|
try: |
|
702
|
|
|
(liveaction_db, execution_db) = action_service.request_cancellation( |
|
703
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
|
704
|
|
|
except: |
|
705
|
|
|
LOG.exception('Failed requesting cancellation for liveaction %s.', liveaction_db.id) |
|
706
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, 'Failed canceling execution.') |
|
707
|
|
|
|
|
708
|
|
|
return ActionExecutionAPI.from_model(execution_db, |
|
709
|
|
|
mask_secrets=from_model_kwargs['mask_secrets']) |
|
710
|
|
|
|
|
711
|
|
|
def _get_action_executions(self, exclude_fields=None, include_fields=None, |
|
712
|
|
|
sort=None, offset=0, limit=None, |
|
713
|
|
|
query_options=None, raw_filters=None, from_model_kwargs=None, |
|
714
|
|
|
requester_user=None): |
|
715
|
|
|
""" |
|
716
|
|
|
:param exclude_fields: A list of object fields to exclude. |
|
717
|
|
|
:type exclude_fields: ``list`` |
|
718
|
|
|
""" |
|
719
|
|
|
|
|
720
|
|
|
if limit is None: |
|
721
|
|
|
limit = self.default_limit |
|
722
|
|
|
|
|
723
|
|
|
limit = int(limit) |
|
724
|
|
|
|
|
725
|
|
|
LOG.debug('Retrieving all action executions with filters=%s', raw_filters) |
|
726
|
|
|
return super(ActionExecutionsController, self)._get_all(exclude_fields=exclude_fields, |
|
727
|
|
|
include_fields=include_fields, |
|
728
|
|
|
from_model_kwargs=from_model_kwargs, |
|
729
|
|
|
sort=sort, |
|
730
|
|
|
offset=offset, |
|
731
|
|
|
limit=limit, |
|
732
|
|
|
query_options=query_options, |
|
733
|
|
|
raw_filters=raw_filters, |
|
734
|
|
|
requester_user=requester_user) |
|
735
|
|
|
|
|
736
|
|
|
|
|
737
|
|
|
action_executions_controller = ActionExecutionsController() |
|
738
|
|
|
action_execution_output_controller = ActionExecutionOutputController() |
|
739
|
|
|
action_execution_rerun_controller = ActionExecutionReRunController() |
|
740
|
|
|
action_execution_attribute_controller = ActionExecutionAttributeController() |
|
741
|
|
|
action_execution_children_controller = ActionExecutionChildrenController() |
|
742
|
|
|
|