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