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 httplib |
19
|
|
|
import sys |
20
|
|
|
import traceback |
21
|
|
|
|
22
|
|
|
import jsonschema |
23
|
|
|
from oslo_config import cfg |
24
|
|
|
from six.moves import http_client |
25
|
|
|
|
26
|
|
|
from st2api.controllers.base import BaseRestControllerMixin |
27
|
|
|
from st2api.controllers.resource import ResourceController |
28
|
|
|
from st2api.controllers.v1.executionviews import ExecutionViewsController |
29
|
|
|
from st2api.controllers.v1.executionviews import SUPPORTED_FILTERS |
30
|
|
|
from st2common import log as logging |
31
|
|
|
from st2common.constants import action as action_constants |
32
|
|
|
from st2common.exceptions import actionrunner as runner_exc |
33
|
|
|
from st2common.exceptions import apivalidation as validation_exc |
34
|
|
|
from st2common.exceptions import param as param_exc |
35
|
|
|
from st2common.exceptions import trace as trace_exc |
36
|
|
|
from st2common.models.api.action import LiveActionAPI |
37
|
|
|
from st2common.models.api.action import LiveActionCreateAPI |
38
|
|
|
from st2common.models.api.base import cast_argument_value |
39
|
|
|
from st2common.models.api.execution import ActionExecutionAPI |
40
|
|
|
from st2common.models.db.auth import UserDB |
41
|
|
|
from st2common.persistence.liveaction import LiveAction |
42
|
|
|
from st2common.persistence.execution import ActionExecution |
43
|
|
|
from st2common.router import abort |
44
|
|
|
from st2common.router import Response |
45
|
|
|
from st2common.services import action as action_service |
46
|
|
|
from st2common.services import executions as execution_service |
47
|
|
|
from st2common.services import trace as trace_service |
48
|
|
|
from st2common.services import rbac as rbac_service |
49
|
|
|
from st2common.util import isotime |
50
|
|
|
from st2common.util import action_db as action_utils |
51
|
|
|
from st2common.util import param as param_utils |
52
|
|
|
from st2common.util.jsonify import try_loads |
53
|
|
|
from st2common.rbac.types import PermissionType |
54
|
|
|
from st2common.rbac import utils as rbac_utils |
55
|
|
|
from st2common.rbac.utils import assert_user_has_resource_db_permission |
56
|
|
|
from st2common.rbac.utils import assert_user_is_admin_if_user_query_param_is_provided |
57
|
|
|
|
58
|
|
|
__all__ = [ |
59
|
|
|
'ActionExecutionsController' |
60
|
|
|
] |
61
|
|
|
|
62
|
|
|
LOG = logging.getLogger(__name__) |
63
|
|
|
|
64
|
|
|
# Note: We initialize filters here and not in the constructor |
65
|
|
|
SUPPORTED_EXECUTIONS_FILTERS = copy.deepcopy(SUPPORTED_FILTERS) |
66
|
|
|
SUPPORTED_EXECUTIONS_FILTERS.update({ |
67
|
|
|
'timestamp_gt': 'start_timestamp.gt', |
68
|
|
|
'timestamp_lt': 'start_timestamp.lt' |
69
|
|
|
}) |
70
|
|
|
|
71
|
|
|
MONITOR_THREAD_EMPTY_Q_SLEEP_TIME = 5 |
72
|
|
|
MONITOR_THREAD_NO_WORKERS_SLEEP_TIME = 1 |
73
|
|
|
|
74
|
|
|
|
75
|
|
|
class ActionExecutionsControllerMixin(BaseRestControllerMixin): |
76
|
|
|
""" |
77
|
|
|
Mixin class with shared methods. |
78
|
|
|
""" |
79
|
|
|
|
80
|
|
|
model = ActionExecutionAPI |
81
|
|
|
access = ActionExecution |
82
|
|
|
|
83
|
|
|
# A list of attributes which can be specified using ?exclude_attributes filter |
84
|
|
|
valid_exclude_attributes = [ |
85
|
|
|
'result', |
86
|
|
|
'trigger_instance' |
87
|
|
|
] |
88
|
|
|
|
89
|
|
|
def _handle_schedule_execution(self, liveaction_api, requester_user, context_string=None, |
90
|
|
|
show_secrets=False): |
91
|
|
|
""" |
92
|
|
|
:param liveaction: LiveActionAPI object. |
93
|
|
|
:type liveaction: :class:`LiveActionAPI` |
94
|
|
|
""" |
95
|
|
|
|
96
|
|
|
if not requester_user: |
97
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
98
|
|
|
|
99
|
|
|
# Assert action ref is valid |
100
|
|
|
action_ref = liveaction_api.action |
101
|
|
|
action_db = action_utils.get_action_by_ref(action_ref) |
102
|
|
|
|
103
|
|
|
if not action_db: |
104
|
|
|
message = 'Action "%s" cannot be found.' % action_ref |
105
|
|
|
LOG.warning(message) |
106
|
|
|
abort(http_client.BAD_REQUEST, message) |
107
|
|
|
|
108
|
|
|
# Assert the permissions |
109
|
|
|
assert_user_has_resource_db_permission(user_db=requester_user, resource_db=action_db, |
110
|
|
|
permission_type=PermissionType.ACTION_EXECUTE) |
111
|
|
|
|
112
|
|
|
# Validate that the authenticated user is admin if user query param is provided |
113
|
|
|
user = liveaction_api.user or requester_user.name |
114
|
|
|
assert_user_is_admin_if_user_query_param_is_provided(user_db=requester_user, |
115
|
|
|
user=user) |
116
|
|
|
|
117
|
|
|
try: |
118
|
|
|
return self._schedule_execution(liveaction=liveaction_api, |
119
|
|
|
requester_user=requester_user, |
120
|
|
|
user=user, |
121
|
|
|
context_string=context_string, |
122
|
|
|
show_secrets=show_secrets, |
123
|
|
|
pack=action_db.pack) |
124
|
|
|
except ValueError as e: |
125
|
|
|
LOG.exception('Unable to execute action.') |
126
|
|
|
abort(http_client.BAD_REQUEST, str(e)) |
127
|
|
|
except jsonschema.ValidationError as e: |
128
|
|
|
LOG.exception('Unable to execute action. Parameter validation failed.') |
129
|
|
|
abort(http_client.BAD_REQUEST, re.sub("u'([^']*)'", r"'\1'", e.message)) |
130
|
|
|
except trace_exc.TraceNotFoundException as e: |
131
|
|
|
abort(http_client.BAD_REQUEST, str(e)) |
132
|
|
|
except validation_exc.ValueValidationException as e: |
133
|
|
|
raise e |
134
|
|
|
except Exception as e: |
135
|
|
|
LOG.exception('Unable to execute action. Unexpected error encountered.') |
136
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, str(e)) |
137
|
|
|
|
138
|
|
|
def _schedule_execution(self, |
139
|
|
|
liveaction, |
140
|
|
|
requester_user, |
141
|
|
|
user=None, |
142
|
|
|
context_string=None, |
143
|
|
|
show_secrets=False, |
144
|
|
|
pack=None): |
145
|
|
|
# Initialize execution context if it does not exist. |
146
|
|
|
if not hasattr(liveaction, 'context'): |
147
|
|
|
liveaction.context = dict() |
148
|
|
|
|
149
|
|
|
liveaction.context['user'] = user |
150
|
|
|
liveaction.context['pack'] = pack |
151
|
|
|
LOG.debug('User is: %s' % liveaction.context['user']) |
152
|
|
|
|
153
|
|
|
# Retrieve other st2 context from request header. |
154
|
|
|
if context_string: |
155
|
|
|
context = try_loads(context_string) |
156
|
|
|
if not isinstance(context, dict): |
157
|
|
|
raise ValueError('Unable to convert st2-context from the headers into JSON.') |
158
|
|
|
liveaction.context.update(context) |
159
|
|
|
|
160
|
|
|
# Include RBAC context (if RBAC is available and enabled) |
161
|
|
|
if cfg.CONF.rbac.enable: |
162
|
|
|
user_db = UserDB(name=user) |
163
|
|
|
role_dbs = rbac_service.get_roles_for_user(user_db=user_db, include_remote=True) |
164
|
|
|
roles = [role_db.name for role_db in role_dbs] |
165
|
|
|
liveaction.context['rbac'] = { |
166
|
|
|
'user': user, |
167
|
|
|
'roles': roles |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
# Schedule the action execution. |
171
|
|
|
liveaction_db = LiveActionAPI.to_model(liveaction) |
172
|
|
|
liveaction_db, actionexecution_db = action_service.create_request(liveaction_db) |
173
|
|
|
|
174
|
|
|
action_db = action_utils.get_action_by_ref(liveaction_db.action) |
175
|
|
|
runnertype_db = action_utils.get_runnertype_by_name(action_db.runner_type['name']) |
176
|
|
|
|
177
|
|
|
try: |
178
|
|
|
liveaction_db.parameters = param_utils.render_live_params( |
179
|
|
|
runnertype_db.runner_parameters, action_db.parameters, liveaction_db.parameters, |
180
|
|
|
liveaction_db.context) |
181
|
|
|
except param_exc.ParamException: |
182
|
|
|
# By this point the execution is already in the DB therefore need to mark it failed. |
183
|
|
|
_, e, tb = sys.exc_info() |
184
|
|
|
action_service.update_status( |
185
|
|
|
liveaction=liveaction_db, |
186
|
|
|
new_status=action_constants.LIVEACTION_STATUS_FAILED, |
187
|
|
|
result={'error': str(e), 'traceback': ''.join(traceback.format_tb(tb, 20))}) |
188
|
|
|
# Might be a good idea to return the actual ActionExecution rather than bubble up |
189
|
|
|
# the execption. |
190
|
|
|
raise validation_exc.ValueValidationException(str(e)) |
191
|
|
|
|
192
|
|
|
liveaction_db = LiveAction.add_or_update(liveaction_db, publish=False) |
193
|
|
|
|
194
|
|
|
_, actionexecution_db = action_service.publish_request(liveaction_db, actionexecution_db) |
195
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
196
|
|
|
execution_api = ActionExecutionAPI.from_model(actionexecution_db, mask_secrets=mask_secrets) |
197
|
|
|
|
198
|
|
|
return Response(json=execution_api, status=http_client.CREATED) |
199
|
|
|
|
200
|
|
|
def _get_result_object(self, id): |
|
|
|
|
201
|
|
|
""" |
202
|
|
|
Retrieve result object for the provided action execution. |
203
|
|
|
|
204
|
|
|
:param id: Action execution ID. |
205
|
|
|
:type id: ``str`` |
206
|
|
|
|
207
|
|
|
:rtype: ``dict`` |
208
|
|
|
""" |
209
|
|
|
fields = ['result'] |
210
|
|
|
action_exec_db = self.access.impl.model.objects.filter(id=id).only(*fields).get() |
211
|
|
|
return action_exec_db.result |
212
|
|
|
|
213
|
|
|
def _get_children(self, id_, requester_user, depth=-1, result_fmt=None, |
214
|
|
|
show_secrets=False): |
215
|
|
|
# make sure depth is int. Url encoding will make it a string and needs to |
216
|
|
|
# be converted back in that case. |
217
|
|
|
depth = int(depth) |
218
|
|
|
LOG.debug('retrieving children for id: %s with depth: %s', id_, depth) |
219
|
|
|
descendants = execution_service.get_descendants(actionexecution_id=id_, |
220
|
|
|
descendant_depth=depth, |
221
|
|
|
result_fmt=result_fmt) |
222
|
|
|
|
223
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
224
|
|
|
return [self.model.from_model(descendant, mask_secrets=mask_secrets) for |
225
|
|
|
descendant in descendants] |
226
|
|
|
|
227
|
|
|
|
228
|
|
|
class BaseActionExecutionNestedController(ActionExecutionsControllerMixin, ResourceController): |
229
|
|
|
# Note: We need to override "get_one" and "get_all" to return 404 since nested controller |
230
|
|
|
# don't implement thos methods |
231
|
|
|
|
232
|
|
|
# ResourceController attributes |
233
|
|
|
query_options = {} |
234
|
|
|
supported_filters = {} |
235
|
|
|
|
236
|
|
|
def get_all(self): |
237
|
|
|
abort(httplib.NOT_FOUND) |
238
|
|
|
|
239
|
|
|
def get_one(self, id): |
|
|
|
|
240
|
|
|
abort(httplib.NOT_FOUND) |
241
|
|
|
|
242
|
|
|
|
243
|
|
|
class ActionExecutionChildrenController(BaseActionExecutionNestedController): |
244
|
|
|
def get_one(self, id, requester_user, depth=-1, result_fmt=None, show_secrets=False): |
|
|
|
|
245
|
|
|
""" |
246
|
|
|
Retrieve children for the provided action execution. |
247
|
|
|
|
248
|
|
|
:rtype: ``list`` |
249
|
|
|
""" |
250
|
|
|
|
251
|
|
|
instance = self._get_by_id(resource_id=id) |
252
|
|
|
|
253
|
|
|
permission_type = PermissionType.EXECUTION_VIEW |
254
|
|
|
rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user, |
255
|
|
|
resource_db=instance, |
256
|
|
|
permission_type=permission_type) |
257
|
|
|
|
258
|
|
|
return self._get_children(id_=id, depth=depth, result_fmt=result_fmt, |
259
|
|
|
requester_user=requester_user, show_secrets=show_secrets) |
260
|
|
|
|
261
|
|
|
|
262
|
|
|
class ActionExecutionAttributeController(BaseActionExecutionNestedController): |
263
|
|
|
valid_exclude_attributes = ['action__pack', 'action__uid'] + \ |
264
|
|
|
ActionExecutionsControllerMixin.valid_exclude_attributes |
265
|
|
|
|
266
|
|
|
def get(self, id, attribute, requester_user): |
|
|
|
|
267
|
|
|
""" |
268
|
|
|
Retrieve a particular attribute for the provided action execution. |
269
|
|
|
|
270
|
|
|
Handles requests: |
271
|
|
|
|
272
|
|
|
GET /executions/<id>/attribute/<attribute name> |
273
|
|
|
|
274
|
|
|
:rtype: ``dict`` |
275
|
|
|
""" |
276
|
|
|
fields = [attribute, 'action__pack', 'action__uid'] |
277
|
|
|
fields = self._validate_exclude_fields(fields) |
278
|
|
|
action_exec_db = self.access.impl.model.objects.filter(id=id).only(*fields).get() |
279
|
|
|
|
280
|
|
|
permission_type = PermissionType.EXECUTION_VIEW |
281
|
|
|
rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user, |
282
|
|
|
resource_db=action_exec_db, |
283
|
|
|
permission_type=permission_type) |
284
|
|
|
|
285
|
|
|
result = getattr(action_exec_db, attribute, None) |
286
|
|
|
return result |
287
|
|
|
|
288
|
|
|
|
289
|
|
|
class ActionExecutionReRunController(ActionExecutionsControllerMixin, ResourceController): |
290
|
|
|
supported_filters = {} |
291
|
|
|
exclude_fields = [ |
292
|
|
|
'result', |
293
|
|
|
'trigger_instance' |
294
|
|
|
] |
295
|
|
|
|
296
|
|
|
class ExecutionSpecificationAPI(object): |
297
|
|
|
def __init__(self, parameters=None, tasks=None, reset=None, user=None): |
298
|
|
|
self.parameters = parameters or {} |
299
|
|
|
self.tasks = tasks or [] |
300
|
|
|
self.reset = reset or [] |
301
|
|
|
self.user = user |
302
|
|
|
|
303
|
|
|
def validate(self): |
304
|
|
|
if (self.tasks or self.reset) and self.parameters: |
305
|
|
|
raise ValueError('Parameters override is not supported when ' |
306
|
|
|
're-running task(s) for a workflow.') |
307
|
|
|
|
308
|
|
|
if self.parameters: |
309
|
|
|
assert isinstance(self.parameters, dict) |
310
|
|
|
|
311
|
|
|
if self.tasks: |
312
|
|
|
assert isinstance(self.tasks, list) |
313
|
|
|
|
314
|
|
|
if self.reset: |
315
|
|
|
assert isinstance(self.reset, list) |
316
|
|
|
|
317
|
|
|
if list(set(self.reset) - set(self.tasks)): |
318
|
|
|
raise ValueError('List of tasks to reset does not match the tasks to rerun.') |
319
|
|
|
|
320
|
|
|
return self |
321
|
|
|
|
322
|
|
|
def post(self, spec_api, id, requester_user, no_merge=False, show_secrets=False): |
|
|
|
|
323
|
|
|
""" |
324
|
|
|
Re-run the provided action execution optionally specifying override parameters. |
325
|
|
|
|
326
|
|
|
Handles requests: |
327
|
|
|
|
328
|
|
|
POST /executions/<id>/re_run |
329
|
|
|
""" |
330
|
|
|
|
331
|
|
|
if (spec_api.tasks or spec_api.reset) and spec_api.parameters: |
332
|
|
|
raise ValueError('Parameters override is not supported when ' |
333
|
|
|
're-running task(s) for a workflow.') |
334
|
|
|
|
335
|
|
|
if spec_api.parameters: |
336
|
|
|
assert isinstance(spec_api.parameters, dict) |
337
|
|
|
|
338
|
|
|
if spec_api.tasks: |
339
|
|
|
assert isinstance(spec_api.tasks, list) |
340
|
|
|
|
341
|
|
|
if spec_api.reset: |
342
|
|
|
assert isinstance(spec_api.reset, list) |
343
|
|
|
|
344
|
|
|
if list(set(spec_api.reset) - set(spec_api.tasks)): |
345
|
|
|
raise ValueError('List of tasks to reset does not match the tasks to rerun.') |
346
|
|
|
|
347
|
|
|
no_merge = cast_argument_value(value_type=bool, value=no_merge) |
348
|
|
|
existing_execution = self._get_one_by_id(id=id, exclude_fields=self.exclude_fields, |
349
|
|
|
requester_user=requester_user, |
350
|
|
|
permission_type=PermissionType.EXECUTION_VIEW) |
351
|
|
|
|
352
|
|
|
if spec_api.tasks and existing_execution.runner['name'] != 'mistral-v2': |
353
|
|
|
raise ValueError('Task option is only supported for Mistral workflows.') |
354
|
|
|
|
355
|
|
|
# Merge in any parameters provided by the user |
356
|
|
|
new_parameters = {} |
357
|
|
|
if not no_merge: |
358
|
|
|
new_parameters.update(getattr(existing_execution, 'parameters', {})) |
359
|
|
|
new_parameters.update(spec_api.parameters) |
360
|
|
|
|
361
|
|
|
# Create object for the new execution |
362
|
|
|
action_ref = existing_execution.action['ref'] |
363
|
|
|
|
364
|
|
|
# Include additional option(s) for the execution |
365
|
|
|
context = { |
366
|
|
|
're-run': { |
367
|
|
|
'ref': id, |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
if spec_api.tasks: |
372
|
|
|
context['re-run']['tasks'] = spec_api.tasks |
373
|
|
|
|
374
|
|
|
if spec_api.reset: |
375
|
|
|
context['re-run']['reset'] = spec_api.reset |
376
|
|
|
|
377
|
|
|
# Add trace to the new execution |
378
|
|
|
trace = trace_service.get_trace_db_by_action_execution( |
379
|
|
|
action_execution_id=existing_execution.id) |
380
|
|
|
|
381
|
|
|
if trace: |
382
|
|
|
context['trace_context'] = {'id_': str(trace.id)} |
383
|
|
|
|
384
|
|
|
new_liveaction_api = LiveActionCreateAPI(action=action_ref, |
385
|
|
|
context=context, |
386
|
|
|
parameters=new_parameters, |
387
|
|
|
user=spec_api.user) |
388
|
|
|
|
389
|
|
|
return self._handle_schedule_execution(liveaction_api=new_liveaction_api, |
390
|
|
|
requester_user=requester_user, |
391
|
|
|
show_secrets=show_secrets) |
392
|
|
|
|
393
|
|
|
|
394
|
|
|
class ActionExecutionsController(ActionExecutionsControllerMixin, ResourceController): |
395
|
|
|
""" |
396
|
|
|
Implements the RESTful web endpoint that handles |
397
|
|
|
the lifecycle of ActionExecutions in the system. |
398
|
|
|
""" |
399
|
|
|
|
400
|
|
|
# Nested controllers |
401
|
|
|
views = ExecutionViewsController() |
402
|
|
|
|
403
|
|
|
children = ActionExecutionChildrenController() |
404
|
|
|
attribute = ActionExecutionAttributeController() |
405
|
|
|
re_run = ActionExecutionReRunController() |
406
|
|
|
|
407
|
|
|
# ResourceController attributes |
408
|
|
|
query_options = { |
409
|
|
|
'sort': ['-start_timestamp', 'action.ref'] |
410
|
|
|
} |
411
|
|
|
supported_filters = SUPPORTED_EXECUTIONS_FILTERS |
412
|
|
|
filter_transform_functions = { |
413
|
|
|
'timestamp_gt': lambda value: isotime.parse(value=value), |
414
|
|
|
'timestamp_lt': lambda value: isotime.parse(value=value) |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
def get_all(self, requester_user, exclude_attributes=None, sort=None, offset=0, limit=None, |
418
|
|
|
show_secrets=False, **raw_filters): |
419
|
|
|
""" |
420
|
|
|
List all executions. |
421
|
|
|
|
422
|
|
|
Handles requests: |
423
|
|
|
GET /executions[?exclude_attributes=result,trigger_instance] |
424
|
|
|
|
425
|
|
|
:param exclude_attributes: Comma delimited string of attributes to exclude from the object. |
426
|
|
|
:type exclude_attributes: ``str`` |
427
|
|
|
""" |
428
|
|
|
if exclude_attributes: |
429
|
|
|
exclude_fields = exclude_attributes.split(',') |
430
|
|
|
else: |
431
|
|
|
exclude_fields = None |
432
|
|
|
|
433
|
|
|
exclude_fields = self._validate_exclude_fields(exclude_fields=exclude_fields) |
434
|
|
|
|
435
|
|
|
# Use a custom sort order when filtering on a timestamp so we return a correct result as |
436
|
|
|
# expected by the user |
437
|
|
|
query_options = None |
438
|
|
|
if raw_filters.get('timestamp_lt', None) or raw_filters.get('sort_desc', None): |
439
|
|
|
query_options = {'sort': ['-start_timestamp', 'action.ref']} |
440
|
|
|
elif raw_filters.get('timestamp_gt', None) or raw_filters.get('sort_asc', None): |
441
|
|
|
query_options = {'sort': ['+start_timestamp', 'action.ref']} |
442
|
|
|
|
443
|
|
|
from_model_kwargs = { |
444
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
445
|
|
|
} |
446
|
|
|
return self._get_action_executions(exclude_fields=exclude_fields, |
447
|
|
|
from_model_kwargs=from_model_kwargs, |
448
|
|
|
sort=sort, |
449
|
|
|
offset=offset, |
450
|
|
|
limit=limit, |
451
|
|
|
query_options=query_options, |
452
|
|
|
raw_filters=raw_filters) |
453
|
|
|
|
454
|
|
|
def get_one(self, id, requester_user, exclude_attributes=None, show_secrets=False): |
|
|
|
|
455
|
|
|
""" |
456
|
|
|
Retrieve a single execution. |
457
|
|
|
|
458
|
|
|
Handles requests: |
459
|
|
|
GET /executions/<id>[?exclude_attributes=result,trigger_instance] |
460
|
|
|
|
461
|
|
|
:param exclude_attributes: Comma delimited string of attributes to exclude from the object. |
462
|
|
|
:type exclude_attributes: ``str`` |
463
|
|
|
""" |
464
|
|
|
if exclude_attributes: |
465
|
|
|
exclude_fields = exclude_attributes.split(',') |
466
|
|
|
else: |
467
|
|
|
exclude_fields = None |
468
|
|
|
|
469
|
|
|
exclude_fields = self._validate_exclude_fields(exclude_fields=exclude_fields) |
470
|
|
|
|
471
|
|
|
from_model_kwargs = { |
472
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
473
|
|
|
} |
474
|
|
|
return self._get_one_by_id(id=id, exclude_fields=exclude_fields, |
475
|
|
|
requester_user=requester_user, |
476
|
|
|
from_model_kwargs=from_model_kwargs, |
477
|
|
|
permission_type=PermissionType.EXECUTION_VIEW) |
478
|
|
|
|
479
|
|
|
def post(self, liveaction_api, requester_user, context_string=None, show_secrets=False): |
480
|
|
|
return self._handle_schedule_execution(liveaction_api=liveaction_api, |
481
|
|
|
requester_user=requester_user, |
482
|
|
|
context_string=context_string, |
483
|
|
|
show_secrets=show_secrets) |
484
|
|
|
|
485
|
|
|
def put(self, id, liveaction_api, requester_user, show_secrets=False): |
|
|
|
|
486
|
|
|
""" |
487
|
|
|
Updates a single execution. |
488
|
|
|
|
489
|
|
|
Handles requests: |
490
|
|
|
PUT /executions/<id> |
491
|
|
|
|
492
|
|
|
""" |
493
|
|
|
if not requester_user: |
494
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
495
|
|
|
|
496
|
|
|
from_model_kwargs = { |
497
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
execution_api = self._get_one_by_id(id=id, requester_user=requester_user, |
501
|
|
|
from_model_kwargs=from_model_kwargs, |
502
|
|
|
permission_type=PermissionType.EXECUTION_STOP) |
503
|
|
|
|
504
|
|
|
if not execution_api: |
505
|
|
|
abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) |
506
|
|
|
|
507
|
|
|
liveaction_id = execution_api.liveaction['id'] |
508
|
|
|
if not liveaction_id: |
509
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
510
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
511
|
|
|
|
512
|
|
|
try: |
513
|
|
|
liveaction_db = LiveAction.get_by_id(liveaction_id) |
514
|
|
|
except: |
515
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
516
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
517
|
|
|
|
518
|
|
|
if liveaction_db.status in action_constants.LIVEACTION_COMPLETED_STATES: |
519
|
|
|
abort(http_client.BAD_REQUEST, 'Execution is already in completed state.') |
520
|
|
|
|
521
|
|
|
if (getattr(liveaction_api, 'result', None) is not None and |
522
|
|
|
liveaction_api.status in [ |
523
|
|
|
action_constants.LIVEACTION_STATUS_PAUSING, |
524
|
|
|
action_constants.LIVEACTION_STATUS_PAUSED, |
525
|
|
|
action_constants.LIVEACTION_STATUS_RESUMING]): |
526
|
|
|
abort(http_client.BAD_REQUEST, |
527
|
|
|
'The result is not applicable for pausing and resuming execution.') |
528
|
|
|
|
529
|
|
|
try: |
530
|
|
|
if (liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSING or |
531
|
|
|
liveaction_api.status == action_constants.LIVEACTION_STATUS_PAUSED): |
532
|
|
|
liveaction_db, actionexecution_db = action_service.request_pause( |
533
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
534
|
|
|
elif liveaction_api.status == action_constants.LIVEACTION_STATUS_RESUMING: |
535
|
|
|
liveaction_db, actionexecution_db = action_service.request_resume( |
536
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
537
|
|
|
else: |
538
|
|
|
liveaction_db = action_service.update_status( |
539
|
|
|
liveaction_db, |
540
|
|
|
liveaction_api.status, |
541
|
|
|
result=getattr(liveaction_api, 'result', None) |
542
|
|
|
) |
543
|
|
|
|
544
|
|
|
actionexecution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) |
545
|
|
|
except runner_exc.InvalidActionRunnerOperationError as e: |
546
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
547
|
|
|
abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) |
548
|
|
|
except runner_exc.UnexpectedActionExecutionStatusError as e: |
549
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
550
|
|
|
abort(http_client.BAD_REQUEST, 'Failed updating execution. %s' % str(e)) |
551
|
|
|
except Exception as e: |
552
|
|
|
LOG.exception('Failed updating liveaction %s. %s', liveaction_db.id, str(e)) |
553
|
|
|
abort( |
554
|
|
|
http_client.INTERNAL_SERVER_ERROR, |
555
|
|
|
'Failed updating execution due to unexpected error.' |
556
|
|
|
) |
557
|
|
|
|
558
|
|
|
mask_secrets = self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
559
|
|
|
execution_api = ActionExecutionAPI.from_model(actionexecution_db, mask_secrets=mask_secrets) |
560
|
|
|
|
561
|
|
|
return execution_api |
562
|
|
|
|
563
|
|
|
def delete(self, id, requester_user, show_secrets=False): |
|
|
|
|
564
|
|
|
""" |
565
|
|
|
Stops a single execution. |
566
|
|
|
|
567
|
|
|
Handles requests: |
568
|
|
|
DELETE /executions/<id> |
569
|
|
|
|
570
|
|
|
""" |
571
|
|
|
if not requester_user: |
572
|
|
|
requester_user = UserDB(cfg.CONF.system_user.user) |
573
|
|
|
|
574
|
|
|
from_model_kwargs = { |
575
|
|
|
'mask_secrets': self._get_mask_secrets(requester_user, show_secrets=show_secrets) |
576
|
|
|
} |
577
|
|
|
execution_api = self._get_one_by_id(id=id, requester_user=requester_user, |
578
|
|
|
from_model_kwargs=from_model_kwargs, |
579
|
|
|
permission_type=PermissionType.EXECUTION_STOP) |
580
|
|
|
|
581
|
|
|
if not execution_api: |
582
|
|
|
abort(http_client.NOT_FOUND, 'Execution with id %s not found.' % id) |
583
|
|
|
|
584
|
|
|
liveaction_id = execution_api.liveaction['id'] |
585
|
|
|
if not liveaction_id: |
586
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
587
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
588
|
|
|
|
589
|
|
|
try: |
590
|
|
|
liveaction_db = LiveAction.get_by_id(liveaction_id) |
591
|
|
|
except: |
592
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, |
593
|
|
|
'Execution object missing link to liveaction %s.' % liveaction_id) |
594
|
|
|
|
595
|
|
|
if liveaction_db.status == action_constants.LIVEACTION_STATUS_CANCELED: |
596
|
|
|
LOG.info( |
597
|
|
|
'Action %s already in "canceled" state; \ |
598
|
|
|
returning execution object.' % liveaction_db.id |
599
|
|
|
) |
600
|
|
|
return execution_api |
601
|
|
|
|
602
|
|
|
if liveaction_db.status not in action_constants.LIVEACTION_CANCELABLE_STATES: |
603
|
|
|
abort(http_client.OK, 'Action cannot be canceled. State = %s.' % liveaction_db.status) |
604
|
|
|
|
605
|
|
|
try: |
606
|
|
|
(liveaction_db, execution_db) = action_service.request_cancellation( |
607
|
|
|
liveaction_db, requester_user.name or cfg.CONF.system_user.user) |
608
|
|
|
except: |
609
|
|
|
LOG.exception('Failed requesting cancellation for liveaction %s.', liveaction_db.id) |
610
|
|
|
abort(http_client.INTERNAL_SERVER_ERROR, 'Failed canceling execution.') |
611
|
|
|
|
612
|
|
|
return ActionExecutionAPI.from_model(execution_db, |
613
|
|
|
mask_secrets=from_model_kwargs['mask_secrets']) |
614
|
|
|
|
615
|
|
|
def _get_action_executions(self, exclude_fields=None, sort=None, offset=0, limit=None, |
616
|
|
|
query_options=None, raw_filters=None, from_model_kwargs=None): |
617
|
|
|
""" |
618
|
|
|
:param exclude_fields: A list of object fields to exclude. |
619
|
|
|
:type exclude_fields: ``list`` |
620
|
|
|
""" |
621
|
|
|
|
622
|
|
|
if limit is None: |
623
|
|
|
limit = self.default_limit |
624
|
|
|
|
625
|
|
|
limit = int(limit) |
626
|
|
|
|
627
|
|
|
LOG.debug('Retrieving all action executions with filters=%s', raw_filters) |
628
|
|
|
return super(ActionExecutionsController, self)._get_all(exclude_fields=exclude_fields, |
629
|
|
|
from_model_kwargs=from_model_kwargs, |
630
|
|
|
sort=sort, |
631
|
|
|
offset=offset, |
632
|
|
|
limit=limit, |
633
|
|
|
query_options=query_options, |
634
|
|
|
raw_filters=raw_filters) |
635
|
|
|
|
636
|
|
|
|
637
|
|
|
action_executions_controller = ActionExecutionsController() |
638
|
|
|
action_execution_rerun_controller = ActionExecutionReRunController() |
639
|
|
|
action_execution_attribute_controller = ActionExecutionAttributeController() |
640
|
|
|
action_execution_children_controller = ActionExecutionChildrenController() |
641
|
|
|
|
It is generally discouraged to redefine built-ins as this makes code very hard to read.