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 os |
17
|
|
|
import ast |
18
|
|
|
import copy |
19
|
|
|
import json |
20
|
|
|
import logging |
21
|
|
|
import textwrap |
22
|
|
|
import calendar |
23
|
|
|
import time |
24
|
|
|
import six |
25
|
|
|
import sys |
26
|
|
|
|
27
|
|
|
from os.path import join as pjoin |
28
|
|
|
|
29
|
|
|
from st2client import models |
30
|
|
|
from st2client.commands import resource |
31
|
|
|
from st2client.commands.resource import ResourceNotFoundError |
32
|
|
|
from st2client.commands.resource import add_auth_token_to_kwargs_from_cli |
33
|
|
|
from st2client.formatters import table |
34
|
|
|
from st2client.formatters import execution as execution_formatter |
35
|
|
|
from st2client.utils import jsutil |
36
|
|
|
from st2client.utils.date import format_isodate_for_user_timezone |
37
|
|
|
from st2client.utils.date import parse as parse_isotime |
38
|
|
|
from st2client.utils.color import format_status |
39
|
|
|
|
40
|
|
|
LOG = logging.getLogger(__name__) |
41
|
|
|
|
42
|
|
|
LIVEACTION_STATUS_REQUESTED = 'requested' |
43
|
|
|
LIVEACTION_STATUS_SCHEDULED = 'scheduled' |
44
|
|
|
LIVEACTION_STATUS_DELAYED = 'delayed' |
45
|
|
|
LIVEACTION_STATUS_RUNNING = 'running' |
46
|
|
|
LIVEACTION_STATUS_SUCCEEDED = 'succeeded' |
47
|
|
|
LIVEACTION_STATUS_FAILED = 'failed' |
48
|
|
|
LIVEACTION_STATUS_TIMED_OUT = 'timeout' |
49
|
|
|
LIVEACTION_STATUS_ABANDONED = 'abandoned' |
50
|
|
|
LIVEACTION_STATUS_CANCELING = 'canceling' |
51
|
|
|
LIVEACTION_STATUS_CANCELED = 'canceled' |
52
|
|
|
LIVEACTION_STATUS_PAUSING = 'pausing' |
53
|
|
|
LIVEACTION_STATUS_PAUSED = 'paused' |
54
|
|
|
LIVEACTION_STATUS_RESUMING = 'resuming' |
55
|
|
|
|
56
|
|
|
LIVEACTION_COMPLETED_STATES = [ |
57
|
|
|
LIVEACTION_STATUS_SUCCEEDED, |
58
|
|
|
LIVEACTION_STATUS_FAILED, |
59
|
|
|
LIVEACTION_STATUS_TIMED_OUT, |
60
|
|
|
LIVEACTION_STATUS_CANCELED, |
61
|
|
|
LIVEACTION_STATUS_ABANDONED |
62
|
|
|
] |
63
|
|
|
|
64
|
|
|
# Who parameters should be masked when displaying action execution output |
65
|
|
|
PARAMETERS_TO_MASK = [ |
66
|
|
|
'password', |
67
|
|
|
'private_key' |
68
|
|
|
] |
69
|
|
|
|
70
|
|
|
# A list of environment variables which are never inherited when using run |
71
|
|
|
# --inherit-env flag |
72
|
|
|
ENV_VARS_BLACKLIST = [ |
73
|
|
|
'pwd', |
74
|
|
|
'mail', |
75
|
|
|
'username', |
76
|
|
|
'user', |
77
|
|
|
'path', |
78
|
|
|
'home', |
79
|
|
|
'ps1', |
80
|
|
|
'shell', |
81
|
|
|
'pythonpath', |
82
|
|
|
'ssh_tty', |
83
|
|
|
'ssh_connection', |
84
|
|
|
'lang', |
85
|
|
|
'ls_colors', |
86
|
|
|
'logname', |
87
|
|
|
'oldpwd', |
88
|
|
|
'term', |
89
|
|
|
'xdg_session_id' |
90
|
|
|
] |
91
|
|
|
|
92
|
|
|
WORKFLOW_RUNNER_TYPES = [ |
93
|
|
|
'action-chain', |
94
|
|
|
'mistral-v2', |
95
|
|
|
] |
96
|
|
|
|
97
|
|
|
|
98
|
|
|
def format_parameters(value): |
99
|
|
|
# Mask sensitive parameters |
100
|
|
|
if not isinstance(value, dict): |
101
|
|
|
# No parameters, leave it as it is |
102
|
|
|
return value |
103
|
|
|
|
104
|
|
|
for param_name, _ in value.items(): |
105
|
|
|
if param_name in PARAMETERS_TO_MASK: |
106
|
|
|
value[param_name] = '********' |
107
|
|
|
|
108
|
|
|
return value |
109
|
|
|
|
110
|
|
|
|
111
|
|
|
# String for indenting etc. |
112
|
|
|
WF_PREFIX = '+ ' |
113
|
|
|
NON_WF_PREFIX = ' ' |
114
|
|
|
INDENT_CHAR = ' ' |
115
|
|
|
|
116
|
|
|
|
117
|
|
|
def format_wf_instances(instances): |
118
|
|
|
""" |
119
|
|
|
Adds identification characters to a workflow and appropriately shifts |
120
|
|
|
the non-workflow instances. If no workflows are found does nothing. |
121
|
|
|
""" |
122
|
|
|
# only add extr chars if there are workflows. |
123
|
|
|
has_wf = False |
124
|
|
|
for instance in instances: |
125
|
|
|
if not getattr(instance, 'children', None): |
126
|
|
|
continue |
127
|
|
|
else: |
128
|
|
|
has_wf = True |
129
|
|
|
break |
130
|
|
|
if not has_wf: |
131
|
|
|
return instances |
132
|
|
|
# Prepend wf and non_wf prefixes. |
133
|
|
|
for instance in instances: |
134
|
|
|
if getattr(instance, 'children', None): |
135
|
|
|
instance.id = WF_PREFIX + instance.id |
136
|
|
|
else: |
137
|
|
|
instance.id = NON_WF_PREFIX + instance.id |
138
|
|
|
return instances |
139
|
|
|
|
140
|
|
|
|
141
|
|
|
def format_execution_statuses(instances): |
142
|
|
|
result = [] |
143
|
|
|
for instance in instances: |
144
|
|
|
instance = format_execution_status(instance) |
145
|
|
|
result.append(instance) |
146
|
|
|
|
147
|
|
|
return result |
148
|
|
|
|
149
|
|
|
|
150
|
|
|
def format_execution_status(instance): |
151
|
|
|
""" |
152
|
|
|
Augment instance "status" attribute with number of seconds which have elapsed for all the |
153
|
|
|
executions which are in running state and execution total run time for all the executions |
154
|
|
|
which have finished. |
155
|
|
|
""" |
156
|
|
|
start_timestamp = getattr(instance, 'start_timestamp', None) |
157
|
|
|
end_timestamp = getattr(instance, 'end_timestamp', None) |
158
|
|
|
|
159
|
|
|
if instance.status == LIVEACTION_STATUS_RUNNING and start_timestamp: |
160
|
|
|
start_timestamp = instance.start_timestamp |
161
|
|
|
start_timestamp = parse_isotime(start_timestamp) |
162
|
|
|
start_timestamp = calendar.timegm(start_timestamp.timetuple()) |
163
|
|
|
now = int(time.time()) |
164
|
|
|
elapsed_seconds = (now - start_timestamp) |
165
|
|
|
instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds) |
166
|
|
|
elif instance.status in LIVEACTION_COMPLETED_STATES and start_timestamp and end_timestamp: |
167
|
|
|
start_timestamp = parse_isotime(start_timestamp) |
168
|
|
|
start_timestamp = calendar.timegm(start_timestamp.timetuple()) |
169
|
|
|
end_timestamp = parse_isotime(end_timestamp) |
170
|
|
|
end_timestamp = calendar.timegm(end_timestamp.timetuple()) |
171
|
|
|
|
172
|
|
|
elapsed_seconds = (end_timestamp - start_timestamp) |
173
|
|
|
instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds) |
174
|
|
|
|
175
|
|
|
return instance |
176
|
|
|
|
177
|
|
|
|
178
|
|
|
class ActionBranch(resource.ResourceBranch): |
179
|
|
|
|
180
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
181
|
|
|
super(ActionBranch, self).__init__( |
182
|
|
|
models.Action, description, app, subparsers, |
183
|
|
|
parent_parser=parent_parser, |
184
|
|
|
commands={ |
185
|
|
|
'list': ActionListCommand, |
186
|
|
|
'get': ActionGetCommand, |
187
|
|
|
'update': ActionUpdateCommand, |
188
|
|
|
'delete': ActionDeleteCommand |
189
|
|
|
}) |
190
|
|
|
|
191
|
|
|
# Registers extended commands |
192
|
|
|
self.commands['enable'] = ActionEnableCommand(self.resource, self.app, self.subparsers) |
193
|
|
|
self.commands['disable'] = ActionDisableCommand(self.resource, self.app, self.subparsers) |
194
|
|
|
self.commands['execute'] = ActionRunCommand( |
195
|
|
|
self.resource, self.app, self.subparsers, |
196
|
|
|
add_help=False) |
197
|
|
|
|
198
|
|
|
|
199
|
|
|
class ActionListCommand(resource.ContentPackResourceListCommand): |
200
|
|
|
display_attributes = ['ref', 'pack', 'description'] |
201
|
|
|
|
202
|
|
|
|
203
|
|
|
class ActionGetCommand(resource.ContentPackResourceGetCommand): |
204
|
|
|
display_attributes = ['all'] |
205
|
|
|
attribute_display_order = ['id', 'uid', 'ref', 'pack', 'name', 'description', |
206
|
|
|
'enabled', 'entry_point', 'runner_type', |
207
|
|
|
'parameters'] |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
class ActionUpdateCommand(resource.ContentPackResourceUpdateCommand): |
211
|
|
|
pass |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
class ActionEnableCommand(resource.ContentPackResourceEnableCommand): |
215
|
|
|
display_attributes = ['all'] |
216
|
|
|
attribute_display_order = ['id', 'ref', 'pack', 'name', 'description', |
217
|
|
|
'enabled', 'entry_point', 'runner_type', |
218
|
|
|
'parameters'] |
219
|
|
|
|
220
|
|
|
|
221
|
|
|
class ActionDisableCommand(resource.ContentPackResourceDisableCommand): |
222
|
|
|
display_attributes = ['all'] |
223
|
|
|
attribute_display_order = ['id', 'ref', 'pack', 'name', 'description', |
224
|
|
|
'enabled', 'entry_point', 'runner_type', |
225
|
|
|
'parameters'] |
226
|
|
|
|
227
|
|
|
|
228
|
|
|
class ActionDeleteCommand(resource.ContentPackResourceDeleteCommand): |
229
|
|
|
pass |
230
|
|
|
|
231
|
|
|
|
232
|
|
|
class ActionRunCommandMixin(object): |
233
|
|
|
""" |
234
|
|
|
Mixin class which contains utility functions related to action execution. |
235
|
|
|
""" |
236
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
237
|
|
|
'start_timestamp', 'end_timestamp', 'result'] |
238
|
|
|
attribute_display_order = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
239
|
|
|
'start_timestamp', 'end_timestamp', 'result'] |
240
|
|
|
attribute_transform_functions = { |
241
|
|
|
'start_timestamp': format_isodate_for_user_timezone, |
242
|
|
|
'end_timestamp': format_isodate_for_user_timezone, |
243
|
|
|
'parameters': format_parameters, |
244
|
|
|
'status': format_status |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
poll_interval = 2 # how often to poll for execution completion when using sync mode |
248
|
|
|
|
249
|
|
|
def get_resource(self, ref_or_id, **kwargs): |
250
|
|
|
return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs) |
251
|
|
|
|
252
|
|
|
@add_auth_token_to_kwargs_from_cli |
253
|
|
|
def run_and_print(self, args, **kwargs): |
254
|
|
|
if self._print_help(args, **kwargs): |
255
|
|
|
return |
256
|
|
|
|
257
|
|
|
execution = self.run(args, **kwargs) |
258
|
|
|
if args.async: |
259
|
|
|
self.print_output('To get the results, execute:\n st2 execution get %s' % |
260
|
|
|
(execution.id), six.text_type) |
261
|
|
|
self.print_output('\nTo view output in real-time, execute:\n st2 execution ' |
262
|
|
|
'tail %s' % (execution.id), six.text_type) |
263
|
|
|
else: |
264
|
|
|
self._print_execution_details(execution=execution, args=args, **kwargs) |
265
|
|
|
|
266
|
|
|
if execution.status == 'failed': |
267
|
|
|
# Exit with non zero if the action has failed |
268
|
|
|
sys.exit(1) |
269
|
|
|
|
270
|
|
|
def _add_common_options(self): |
271
|
|
|
root_arg_grp = self.parser.add_mutually_exclusive_group() |
272
|
|
|
|
273
|
|
|
# Display options |
274
|
|
|
task_list_arg_grp = root_arg_grp.add_argument_group() |
275
|
|
|
task_list_arg_grp.add_argument('--raw', action='store_true', |
276
|
|
|
help='Raw output, don\'t show sub-tasks for workflows.') |
277
|
|
|
task_list_arg_grp.add_argument('--show-tasks', action='store_true', |
278
|
|
|
help='Whether to show sub-tasks of an execution.') |
279
|
|
|
task_list_arg_grp.add_argument('--depth', type=int, default=-1, |
280
|
|
|
help='Depth to which to show sub-tasks. \ |
281
|
|
|
By default all are shown.') |
282
|
|
|
task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None, |
283
|
|
|
help='Set the width of columns in output.') |
284
|
|
|
|
285
|
|
|
execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group() |
286
|
|
|
|
287
|
|
|
detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
288
|
|
|
detail_arg_grp.add_argument('--attr', nargs='+', |
289
|
|
|
default=['id', 'status', 'parameters', 'result'], |
290
|
|
|
help=('List of attributes to include in the ' |
291
|
|
|
'output. "all" or unspecified will ' |
292
|
|
|
'return all attributes.')) |
293
|
|
|
detail_arg_grp.add_argument('-d', '--detail', action='store_true', |
294
|
|
|
help='Display full detail of the execution in table format.') |
295
|
|
|
|
296
|
|
|
result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
297
|
|
|
result_arg_grp.add_argument('-k', '--key', |
298
|
|
|
help=('If result is type of JSON, then print specific ' |
299
|
|
|
'key-value pair; dot notation for nested JSON is ' |
300
|
|
|
'supported.')) |
301
|
|
|
|
302
|
|
|
# Other options |
303
|
|
|
detail_arg_grp.add_argument('--tail', action='store_true', |
304
|
|
|
help='Automatically start tailing new execution.') |
305
|
|
|
|
306
|
|
|
return root_arg_grp |
307
|
|
|
|
308
|
|
|
def _print_execution_details(self, execution, args, **kwargs): |
309
|
|
|
""" |
310
|
|
|
Print the execution detail to stdout. |
311
|
|
|
|
312
|
|
|
This method takes into account if an executed action was workflow or not |
313
|
|
|
and formats the output accordingly. |
314
|
|
|
""" |
315
|
|
|
runner_type = execution.action.get('runner_type', 'unknown') |
316
|
|
|
is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES |
317
|
|
|
|
318
|
|
|
show_tasks = getattr(args, 'show_tasks', False) |
319
|
|
|
raw = getattr(args, 'raw', False) |
320
|
|
|
detail = getattr(args, 'detail', False) |
321
|
|
|
key = getattr(args, 'key', None) |
322
|
|
|
attr = getattr(args, 'attr', []) |
323
|
|
|
|
324
|
|
|
if show_tasks and not is_workflow_action: |
325
|
|
|
raise ValueError('--show-tasks option can only be used with workflow actions') |
326
|
|
|
|
327
|
|
|
if not raw and not detail and (show_tasks or is_workflow_action): |
328
|
|
|
self._run_and_print_child_task_list(execution=execution, args=args, **kwargs) |
329
|
|
|
else: |
330
|
|
|
instance = execution |
331
|
|
|
|
332
|
|
|
if detail: |
333
|
|
|
formatter = table.PropertyValueTable |
334
|
|
|
else: |
335
|
|
|
formatter = execution_formatter.ExecutionResult |
336
|
|
|
|
337
|
|
|
if detail: |
338
|
|
|
options = {'attributes': copy.copy(self.display_attributes)} |
339
|
|
|
elif key: |
340
|
|
|
options = {'attributes': ['result.%s' % (key)], 'key': key} |
341
|
|
|
else: |
342
|
|
|
options = {'attributes': attr} |
343
|
|
|
|
344
|
|
|
options['json'] = args.json |
345
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
346
|
|
|
self.print_output(instance, formatter, **options) |
347
|
|
|
|
348
|
|
|
def _run_and_print_child_task_list(self, execution, args, **kwargs): |
349
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
350
|
|
|
|
351
|
|
|
instance = execution |
352
|
|
|
options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp', |
353
|
|
|
'end_timestamp']} |
354
|
|
|
options['json'] = args.json |
355
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
356
|
|
|
formatter = execution_formatter.ExecutionResult |
357
|
|
|
|
358
|
|
|
kwargs['depth'] = args.depth |
359
|
|
|
child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs) |
360
|
|
|
child_instances = self._format_child_instances(child_instances, execution.id) |
361
|
|
|
child_instances = format_execution_statuses(child_instances) |
362
|
|
|
|
363
|
|
|
if not child_instances: |
364
|
|
|
# No child error, there might be a global error, include result in the output |
365
|
|
|
options['attributes'].append('result') |
366
|
|
|
|
367
|
|
|
status_index = options['attributes'].index('status') |
368
|
|
|
|
369
|
|
|
if hasattr(instance, 'result') and isinstance(instance.result, dict): |
370
|
|
|
tasks = instance.result.get('tasks', []) |
371
|
|
|
else: |
372
|
|
|
tasks = [] |
373
|
|
|
|
374
|
|
|
# On failure we also want to include error message and traceback at the top level |
375
|
|
|
if instance.status == 'failed': |
376
|
|
|
top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance) |
377
|
|
|
|
378
|
|
|
if len(tasks) >= 1: |
379
|
|
|
task_error, task_traceback = self._get_task_error(task=tasks[-1]) |
380
|
|
|
else: |
381
|
|
|
task_error, task_traceback = None, None |
382
|
|
|
|
383
|
|
|
if top_level_error: |
384
|
|
|
# Top-level error |
385
|
|
|
instance.error = top_level_error |
386
|
|
|
instance.traceback = top_level_traceback |
387
|
|
|
instance.result = 'See error and traceback.' |
388
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
389
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
390
|
|
|
elif task_error: |
391
|
|
|
# Task error |
392
|
|
|
instance.error = task_error |
393
|
|
|
instance.traceback = task_traceback |
394
|
|
|
instance.result = 'See error and traceback.' |
395
|
|
|
instance.failed_on = tasks[-1].get('name', 'unknown') |
396
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
397
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
398
|
|
|
options['attributes'].insert(status_index + 3, 'failed_on') |
399
|
|
|
|
400
|
|
|
# Include result on the top-level object so user doesn't need to issue another command to |
401
|
|
|
# see the result |
402
|
|
|
if len(tasks) >= 1: |
403
|
|
|
task_result = self._get_task_result(task=tasks[-1]) |
404
|
|
|
|
405
|
|
|
if task_result: |
406
|
|
|
instance.result_task = tasks[-1].get('name', 'unknown') |
407
|
|
|
options['attributes'].insert(status_index + 1, 'result_task') |
408
|
|
|
options['attributes'].insert(status_index + 2, 'result') |
409
|
|
|
instance.result = task_result |
410
|
|
|
|
411
|
|
|
# print root task |
412
|
|
|
self.print_output(instance, formatter, **options) |
413
|
|
|
|
414
|
|
|
# print child tasks |
415
|
|
|
if child_instances: |
416
|
|
|
self.print_output(child_instances, table.MultiColumnTable, |
417
|
|
|
attributes=['id', 'status', 'task', 'action', 'start_timestamp'], |
418
|
|
|
widths=args.width, json=args.json, |
419
|
|
|
yaml=args.yaml, |
420
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
421
|
|
|
|
422
|
|
|
def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): |
423
|
|
|
pending_statuses = [ |
424
|
|
|
LIVEACTION_STATUS_REQUESTED, |
425
|
|
|
LIVEACTION_STATUS_SCHEDULED, |
426
|
|
|
LIVEACTION_STATUS_RUNNING, |
427
|
|
|
LIVEACTION_STATUS_CANCELING |
428
|
|
|
] |
429
|
|
|
|
430
|
|
|
if args.tail: |
431
|
|
|
# Start tailing new execution |
432
|
|
|
print('Tailing execution "%s"' % (str(execution.id))) |
433
|
|
|
execution_manager = self.manager |
434
|
|
|
stream_manager = self.app.client.managers['Stream'] |
435
|
|
|
ActionExecutionTailCommand.tail_execution(execution=execution, |
436
|
|
|
execution_manager=execution_manager, |
437
|
|
|
stream_manager=stream_manager, |
438
|
|
|
**kwargs) |
439
|
|
|
|
440
|
|
|
execution = action_exec_mgr.get_by_id(execution.id, **kwargs) |
441
|
|
|
print('') |
442
|
|
|
return execution |
443
|
|
|
|
444
|
|
|
if not args.async: |
445
|
|
|
while execution.status in pending_statuses: |
446
|
|
|
time.sleep(self.poll_interval) |
447
|
|
|
if not args.json and not args.yaml: |
448
|
|
|
sys.stdout.write('.') |
449
|
|
|
sys.stdout.flush() |
450
|
|
|
execution = action_exec_mgr.get_by_id(execution.id, **kwargs) |
451
|
|
|
|
452
|
|
|
sys.stdout.write('\n') |
453
|
|
|
|
454
|
|
|
if execution.status == LIVEACTION_STATUS_CANCELED: |
455
|
|
|
return execution |
456
|
|
|
|
457
|
|
|
return execution |
458
|
|
|
|
459
|
|
|
def _get_top_level_error(self, live_action): |
460
|
|
|
""" |
461
|
|
|
Retrieve a top level workflow error. |
462
|
|
|
|
463
|
|
|
:return: (error, traceback) |
464
|
|
|
""" |
465
|
|
|
if isinstance(live_action.result, dict): |
466
|
|
|
error = live_action.result.get('error', None) |
467
|
|
|
traceback = live_action.result.get('traceback', None) |
468
|
|
|
else: |
469
|
|
|
error = "See result" |
470
|
|
|
traceback = "See result" |
471
|
|
|
|
472
|
|
|
return error, traceback |
473
|
|
|
|
474
|
|
|
def _get_task_error(self, task): |
475
|
|
|
""" |
476
|
|
|
Retrieve error message from the provided task. |
477
|
|
|
|
478
|
|
|
:return: (error, traceback) |
479
|
|
|
""" |
480
|
|
|
if not task: |
481
|
|
|
return None, None |
482
|
|
|
|
483
|
|
|
result = task['result'] |
484
|
|
|
|
485
|
|
|
if isinstance(result, dict): |
486
|
|
|
stderr = result.get('stderr', None) |
487
|
|
|
error = result.get('error', None) |
488
|
|
|
traceback = result.get('traceback', None) |
489
|
|
|
error = error if error else stderr |
490
|
|
|
else: |
491
|
|
|
stderr = None |
492
|
|
|
error = None |
493
|
|
|
traceback = None |
494
|
|
|
|
495
|
|
|
return error, traceback |
496
|
|
|
|
497
|
|
|
def _get_task_result(self, task): |
498
|
|
|
if not task: |
499
|
|
|
return None |
500
|
|
|
|
501
|
|
|
return task['result'] |
502
|
|
|
|
503
|
|
|
def _get_action_parameters_from_args(self, action, runner, args): |
504
|
|
|
""" |
505
|
|
|
Build a dictionary with parameters which will be passed to the action by |
506
|
|
|
parsing parameters passed to the CLI. |
507
|
|
|
|
508
|
|
|
:param args: CLI argument. |
509
|
|
|
:type args: ``object`` |
510
|
|
|
|
511
|
|
|
:rtype: ``dict`` |
512
|
|
|
""" |
513
|
|
|
action_ref_or_id = action.ref |
514
|
|
|
|
515
|
|
|
def read_file(file_path): |
516
|
|
|
if not os.path.exists(file_path): |
517
|
|
|
raise ValueError('File "%s" doesn\'t exist' % (file_path)) |
518
|
|
|
|
519
|
|
|
if not os.path.isfile(file_path): |
520
|
|
|
raise ValueError('"%s" is not a file' % (file_path)) |
521
|
|
|
|
522
|
|
|
with open(file_path, 'rb') as fp: |
523
|
|
|
content = fp.read() |
524
|
|
|
|
525
|
|
|
return content |
526
|
|
|
|
527
|
|
|
def transform_object(value): |
528
|
|
|
# Also support simple key1=val1,key2=val2 syntax |
529
|
|
|
if value.startswith('{'): |
530
|
|
|
# Assume it's JSON |
531
|
|
|
result = value = json.loads(value) |
532
|
|
|
else: |
533
|
|
|
pairs = value.split(',') |
534
|
|
|
|
535
|
|
|
result = {} |
536
|
|
|
for pair in pairs: |
537
|
|
|
split = pair.split('=', 1) |
538
|
|
|
|
539
|
|
|
if len(split) != 2: |
540
|
|
|
continue |
541
|
|
|
|
542
|
|
|
key, value = split |
543
|
|
|
result[key] = value |
544
|
|
|
return result |
545
|
|
|
|
546
|
|
|
def transform_array(value, action_params=None): |
547
|
|
|
action_params = action_params or {} |
548
|
|
|
|
549
|
|
|
# Sometimes an array parameter only has a single element: |
550
|
|
|
# |
551
|
|
|
# i.e. "st2 run foopack.fooaction arrayparam=51" |
552
|
|
|
# |
553
|
|
|
# Normally, json.loads would throw an exception, and the split method |
554
|
|
|
# would be used. However, since this is an int, not only would |
555
|
|
|
# splitting not work, but json.loads actually treats this as valid JSON, |
556
|
|
|
# but as an int, not an array. This causes a mismatch when the API is called. |
557
|
|
|
# |
558
|
|
|
# We want to try to handle this first, so it doesn't get accidentally |
559
|
|
|
# sent to the API as an int, instead of an array of single-element int. |
560
|
|
|
try: |
561
|
|
|
# Force this to be a list containing the single int, then |
562
|
|
|
# cast the whole thing to string so json.loads can handle it |
563
|
|
|
value = str([int(value)]) |
564
|
|
|
except ValueError: |
565
|
|
|
# Original value wasn't an int, so just let it continue |
566
|
|
|
pass |
567
|
|
|
|
568
|
|
|
# At this point, the input is either a a "json.loads"-able construct |
569
|
|
|
# like [1, 2, 3], or even [1], or it is a comma-separated list, |
570
|
|
|
# Try both, in that order. |
571
|
|
|
try: |
572
|
|
|
result = json.loads(value) |
573
|
|
|
except ValueError: |
574
|
|
|
result = [v.strip() for v in value.split(',')] |
575
|
|
|
|
576
|
|
|
# When each values in this array represent dict type, this converts |
577
|
|
|
# the 'result' to the dict type value. |
578
|
|
|
if all([isinstance(x, str) and ':' in x for x in result]): |
579
|
|
|
result_dict = {} |
580
|
|
|
for (k, v) in [x.split(':') for x in result]: |
581
|
|
|
# To parse values using the 'transformer' according to the type which is |
582
|
|
|
# specified in the action metadata, calling 'normalize' method recursively. |
583
|
|
|
if 'properties' in action_params and k in action_params['properties']: |
584
|
|
|
result_dict[k] = normalize(k, v, action_params['properties']) |
585
|
|
|
else: |
586
|
|
|
result_dict[k] = v |
587
|
|
|
return [result_dict] |
588
|
|
|
|
589
|
|
|
return result |
590
|
|
|
|
591
|
|
|
transformer = { |
592
|
|
|
'array': transform_array, |
593
|
|
|
'boolean': (lambda x: ast.literal_eval(x.capitalize())), |
594
|
|
|
'integer': int, |
595
|
|
|
'number': float, |
596
|
|
|
'object': transform_object, |
597
|
|
|
'string': str |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
def get_param_type(key, action_params=None): |
601
|
|
|
action_params = action_params or action.parameters |
602
|
|
|
|
603
|
|
|
param = None |
604
|
|
|
if key in runner.runner_parameters: |
605
|
|
|
param = runner.runner_parameters[key] |
606
|
|
|
elif key in action_params: |
607
|
|
|
param = action_params[key] |
608
|
|
|
|
609
|
|
|
if param: |
610
|
|
|
return param['type'] |
611
|
|
|
|
612
|
|
|
return None |
613
|
|
|
|
614
|
|
|
def normalize(name, value, action_params=None): |
615
|
|
|
""" The desired type is contained in the action meta-data, so we can look that up |
616
|
|
|
and call the desired "caster" function listed in the "transformer" dict |
617
|
|
|
""" |
618
|
|
|
action_params = action_params or action.parameters |
619
|
|
|
|
620
|
|
|
# By default, this method uses a parameter which is defined in the action metadata. |
621
|
|
|
# This method assume to be called recursively for parsing values in an array of objects |
622
|
|
|
# type value according to the nested action metadata definition. |
623
|
|
|
# |
624
|
|
|
# This is a best practice to pass a list value as default argument to prevent |
625
|
|
|
# unforeseen consequence by being created a persistent object. |
626
|
|
|
|
627
|
|
|
# Users can also specify type for each array parameter inside an action metadata |
628
|
|
|
# (items: type: int for example) and this information is available here so we could |
629
|
|
|
# also leverage that to cast each array item to the correct type. |
630
|
|
|
param_type = get_param_type(name, action_params) |
631
|
|
|
if param_type == 'array' and name in action_params: |
632
|
|
|
return transformer[param_type](value, action_params[name]) |
633
|
|
|
elif param_type: |
634
|
|
|
return transformer[param_type](value) |
635
|
|
|
|
636
|
|
|
return value |
637
|
|
|
|
638
|
|
|
result = {} |
639
|
|
|
|
640
|
|
|
if not args.parameters: |
641
|
|
|
return result |
642
|
|
|
|
643
|
|
|
for idx in range(len(args.parameters)): |
644
|
|
|
arg = args.parameters[idx] |
645
|
|
|
if '=' in arg: |
646
|
|
|
k, v = arg.split('=', 1) |
647
|
|
|
|
648
|
|
|
# Attribute for files are prefixed with "@" |
649
|
|
|
if k.startswith('@'): |
650
|
|
|
k = k[1:] |
651
|
|
|
is_file = True |
652
|
|
|
else: |
653
|
|
|
is_file = False |
654
|
|
|
|
655
|
|
|
try: |
656
|
|
|
if is_file: |
657
|
|
|
# Files are handled a bit differently since we ship the content |
658
|
|
|
# over the wire |
659
|
|
|
file_path = os.path.normpath(pjoin(os.getcwd(), v)) |
660
|
|
|
file_name = os.path.basename(file_path) |
661
|
|
|
content = read_file(file_path=file_path) |
662
|
|
|
|
663
|
|
|
if action_ref_or_id == 'core.http': |
664
|
|
|
# Special case for http runner |
665
|
|
|
result['_file_name'] = file_name |
666
|
|
|
result['file_content'] = content |
667
|
|
|
else: |
668
|
|
|
result[k] = content |
669
|
|
|
else: |
670
|
|
|
# This permits multiple declarations of argument only in the array type. |
671
|
|
|
if get_param_type(k) == 'array' and k in result: |
672
|
|
|
result[k] += normalize(k, v) |
673
|
|
|
else: |
674
|
|
|
result[k] = normalize(k, v) |
675
|
|
|
|
676
|
|
|
except Exception as e: |
677
|
|
|
# TODO: Move transformers in a separate module and handle |
678
|
|
|
# exceptions there |
679
|
|
|
if 'malformed string' in str(e): |
680
|
|
|
message = ('Invalid value for boolean parameter. ' |
681
|
|
|
'Valid values are: true, false') |
682
|
|
|
raise ValueError(message) |
683
|
|
|
else: |
684
|
|
|
raise e |
685
|
|
|
else: |
686
|
|
|
result['cmd'] = ' '.join(args.parameters[idx:]) |
687
|
|
|
break |
688
|
|
|
|
689
|
|
|
# Special case for http runner |
690
|
|
|
if 'file_content' in result: |
691
|
|
|
if 'method' not in result: |
692
|
|
|
# Default to POST if a method is not provided |
693
|
|
|
result['method'] = 'POST' |
694
|
|
|
|
695
|
|
|
if 'file_name' not in result: |
696
|
|
|
# File name not provided, use default file name |
697
|
|
|
result['file_name'] = result['_file_name'] |
698
|
|
|
|
699
|
|
|
del result['_file_name'] |
700
|
|
|
|
701
|
|
|
if args.inherit_env: |
702
|
|
|
result['env'] = self._get_inherited_env_vars() |
703
|
|
|
|
704
|
|
|
return result |
705
|
|
|
|
706
|
|
|
@add_auth_token_to_kwargs_from_cli |
707
|
|
|
def _print_help(self, args, **kwargs): |
708
|
|
|
# Print appropriate help message if the help option is given. |
709
|
|
|
action_mgr = self.app.client.managers['Action'] |
710
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
711
|
|
|
|
712
|
|
|
if args.help: |
713
|
|
|
action_ref_or_id = getattr(args, 'ref_or_id', None) |
714
|
|
|
action_exec_id = getattr(args, 'id', None) |
715
|
|
|
|
716
|
|
|
if action_exec_id and not action_ref_or_id: |
717
|
|
|
action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs) |
718
|
|
|
args.ref_or_id = action_exec.action |
719
|
|
|
|
720
|
|
|
if action_ref_or_id: |
721
|
|
|
try: |
722
|
|
|
action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs) |
723
|
|
|
if not action: |
724
|
|
|
raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id) |
725
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
726
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
727
|
|
|
parameters, required, optional, _ = self._get_params_types(runner, |
728
|
|
|
action) |
729
|
|
|
print('') |
730
|
|
|
print(textwrap.fill(action.description)) |
731
|
|
|
print('') |
732
|
|
|
if required: |
733
|
|
|
required = self._sort_parameters(parameters=parameters, |
734
|
|
|
names=required) |
735
|
|
|
|
736
|
|
|
print('Required Parameters:') |
737
|
|
|
[self._print_param(name, parameters.get(name)) |
738
|
|
|
for name in required] |
739
|
|
|
if optional: |
740
|
|
|
optional = self._sort_parameters(parameters=parameters, |
741
|
|
|
names=optional) |
742
|
|
|
|
743
|
|
|
print('Optional Parameters:') |
744
|
|
|
[self._print_param(name, parameters.get(name)) |
745
|
|
|
for name in optional] |
746
|
|
|
except resource.ResourceNotFoundError: |
747
|
|
|
print(('Action "%s" is not found. ' % args.ref_or_id) + |
748
|
|
|
'Use "st2 action list" to see the list of available actions.') |
749
|
|
|
except Exception as e: |
750
|
|
|
print('ERROR: Unable to print help for action "%s". %s' % |
751
|
|
|
(args.ref_or_id, e)) |
752
|
|
|
else: |
753
|
|
|
self.parser.print_help() |
754
|
|
|
return True |
755
|
|
|
return False |
756
|
|
|
|
757
|
|
|
@staticmethod |
758
|
|
|
def _print_param(name, schema): |
759
|
|
|
if not schema: |
760
|
|
|
raise ValueError('Missing schema for parameter "%s"' % (name)) |
761
|
|
|
|
762
|
|
|
wrapper = textwrap.TextWrapper(width=78) |
763
|
|
|
wrapper.initial_indent = ' ' * 4 |
764
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
765
|
|
|
print(wrapper.fill(name)) |
766
|
|
|
wrapper.initial_indent = ' ' * 8 |
767
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
768
|
|
|
if 'description' in schema and schema['description']: |
769
|
|
|
print(wrapper.fill(schema['description'])) |
770
|
|
|
if 'type' in schema and schema['type']: |
771
|
|
|
print(wrapper.fill('Type: %s' % schema['type'])) |
772
|
|
|
if 'enum' in schema and schema['enum']: |
773
|
|
|
print(wrapper.fill('Enum: %s' % ', '.join(schema['enum']))) |
774
|
|
|
if 'default' in schema and schema['default'] is not None: |
775
|
|
|
print(wrapper.fill('Default: %s' % schema['default'])) |
776
|
|
|
print('') |
777
|
|
|
|
778
|
|
|
@staticmethod |
779
|
|
|
def _get_params_types(runner, action): |
780
|
|
|
runner_params = runner.runner_parameters |
781
|
|
|
action_params = action.parameters |
782
|
|
|
parameters = copy.copy(runner_params) |
783
|
|
|
parameters.update(copy.copy(action_params)) |
784
|
|
|
required = set([k for k, v in six.iteritems(parameters) if v.get('required')]) |
785
|
|
|
|
786
|
|
|
def is_immutable(runner_param_meta, action_param_meta): |
787
|
|
|
# If runner sets a param as immutable, action cannot override that. |
788
|
|
|
if runner_param_meta.get('immutable', False): |
789
|
|
|
return True |
790
|
|
|
else: |
791
|
|
|
return action_param_meta.get('immutable', False) |
792
|
|
|
|
793
|
|
|
immutable = set() |
794
|
|
|
for param in parameters.keys(): |
795
|
|
|
if is_immutable(runner_params.get(param, {}), |
796
|
|
|
action_params.get(param, {})): |
797
|
|
|
immutable.add(param) |
798
|
|
|
|
799
|
|
|
required = required - immutable |
800
|
|
|
optional = set(parameters.keys()) - required - immutable |
801
|
|
|
|
802
|
|
|
return parameters, required, optional, immutable |
803
|
|
|
|
804
|
|
|
def _format_child_instances(self, children, parent_id): |
805
|
|
|
''' |
806
|
|
|
The goal of this method is to add an indent at every level. This way the |
807
|
|
|
WF is represented as a tree structure while in a list. For the right visuals |
808
|
|
|
representation the list must be a DF traversal else the idents will end up |
809
|
|
|
looking strange. |
810
|
|
|
''' |
811
|
|
|
# apply basic WF formating first. |
812
|
|
|
children = format_wf_instances(children) |
813
|
|
|
# setup a depth lookup table |
814
|
|
|
depth = {parent_id: 0} |
815
|
|
|
result = [] |
816
|
|
|
# main loop that indents each entry correctly |
817
|
|
|
for child in children: |
818
|
|
|
# make sure child.parent is in depth and while at it compute the |
819
|
|
|
# right depth for indentation purposes. |
820
|
|
|
if child.parent not in depth: |
821
|
|
|
parent = None |
822
|
|
|
for instance in children: |
823
|
|
|
if WF_PREFIX in instance.id: |
824
|
|
|
instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):] |
825
|
|
|
else: |
826
|
|
|
instance_id = instance.id |
827
|
|
|
if instance_id == child.parent: |
828
|
|
|
parent = instance |
829
|
|
|
if parent and parent.parent and parent.parent in depth: |
830
|
|
|
depth[child.parent] = depth[parent.parent] + 1 |
831
|
|
|
else: |
832
|
|
|
depth[child.parent] = 0 |
833
|
|
|
# now ident for the right visuals |
834
|
|
|
child.id = INDENT_CHAR * depth[child.parent] + child.id |
835
|
|
|
result.append(self._format_for_common_representation(child)) |
836
|
|
|
return result |
837
|
|
|
|
838
|
|
|
def _format_for_common_representation(self, task): |
839
|
|
|
''' |
840
|
|
|
Formats a task for common representation between mistral and action-chain. |
841
|
|
|
''' |
842
|
|
|
# This really needs to be better handled on the back-end but that would be a bigger |
843
|
|
|
# change so handling in cli. |
844
|
|
|
context = getattr(task, 'context', None) |
845
|
|
|
if context and 'chain' in context: |
846
|
|
|
task_name_key = 'context.chain.name' |
847
|
|
|
elif context and 'mistral' in context: |
848
|
|
|
task_name_key = 'context.mistral.task_name' |
849
|
|
|
# Use LiveAction as the object so that the formatter lookup does not change. |
850
|
|
|
# AKA HACK! |
851
|
|
|
return models.action.LiveAction(**{ |
852
|
|
|
'id': task.id, |
853
|
|
|
'status': task.status, |
854
|
|
|
'task': jsutil.get_value(vars(task), task_name_key), |
855
|
|
|
'action': task.action.get('ref', None), |
856
|
|
|
'start_timestamp': task.start_timestamp, |
857
|
|
|
'end_timestamp': getattr(task, 'end_timestamp', None) |
858
|
|
|
}) |
859
|
|
|
|
860
|
|
|
def _sort_parameters(self, parameters, names): |
861
|
|
|
""" |
862
|
|
|
Sort a provided list of action parameters. |
863
|
|
|
|
864
|
|
|
:type parameters: ``list`` |
865
|
|
|
:type names: ``list`` or ``set`` |
866
|
|
|
""" |
867
|
|
|
sorted_parameters = sorted(names, key=lambda name: |
868
|
|
|
self._get_parameter_sort_value( |
869
|
|
|
parameters=parameters, |
870
|
|
|
name=name)) |
871
|
|
|
|
872
|
|
|
return sorted_parameters |
873
|
|
|
|
874
|
|
|
def _get_parameter_sort_value(self, parameters, name): |
875
|
|
|
""" |
876
|
|
|
Return a value which determines sort order for a particular parameter. |
877
|
|
|
|
878
|
|
|
By default, parameters are sorted using "position" parameter attribute. |
879
|
|
|
If this attribute is not available, parameter is sorted based on the |
880
|
|
|
name. |
881
|
|
|
""" |
882
|
|
|
parameter = parameters.get(name, None) |
883
|
|
|
|
884
|
|
|
if not parameter: |
885
|
|
|
return None |
886
|
|
|
|
887
|
|
|
sort_value = parameter.get('position', name) |
888
|
|
|
return sort_value |
889
|
|
|
|
890
|
|
|
def _get_inherited_env_vars(self): |
891
|
|
|
env_vars = os.environ.copy() |
892
|
|
|
|
893
|
|
|
for var_name in ENV_VARS_BLACKLIST: |
894
|
|
|
if var_name.lower() in env_vars: |
895
|
|
|
del env_vars[var_name.lower()] |
896
|
|
|
if var_name.upper() in env_vars: |
897
|
|
|
del env_vars[var_name.upper()] |
898
|
|
|
|
899
|
|
|
return env_vars |
900
|
|
|
|
901
|
|
|
|
902
|
|
|
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
903
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
904
|
|
|
|
905
|
|
|
super(ActionRunCommand, self).__init__( |
906
|
|
|
resource, kwargs.pop('name', 'execute'), |
907
|
|
|
'Invoke an action manually.', |
908
|
|
|
*args, **kwargs) |
909
|
|
|
|
910
|
|
|
self.parser.add_argument('ref_or_id', nargs='?', |
911
|
|
|
metavar='ref-or-id', |
912
|
|
|
help='Action reference (pack.action_name) ' + |
913
|
|
|
'or ID of the action.') |
914
|
|
|
self.parser.add_argument('parameters', nargs='*', |
915
|
|
|
help='List of keyword args, positional args, ' |
916
|
|
|
'and optional args for the action.') |
917
|
|
|
|
918
|
|
|
self.parser.add_argument('-h', '--help', |
919
|
|
|
action='store_true', dest='help', |
920
|
|
|
help='Print usage for the given action.') |
921
|
|
|
|
922
|
|
|
self._add_common_options() |
923
|
|
|
|
924
|
|
|
if self.name in ['run', 'execute']: |
925
|
|
|
self.parser.add_argument('--trace-tag', '--trace_tag', |
926
|
|
|
help='A trace tag string to track execution later.', |
927
|
|
|
dest='trace_tag', required=False) |
928
|
|
|
self.parser.add_argument('--trace-id', |
929
|
|
|
help='Existing trace id for this execution.', |
930
|
|
|
dest='trace_id', required=False) |
931
|
|
|
self.parser.add_argument('-a', '--async', |
932
|
|
|
action='store_true', dest='async', |
933
|
|
|
help='Do not wait for action to finish.') |
934
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
935
|
|
|
action='store_true', dest='inherit_env', |
936
|
|
|
help='Pass all the environment variables ' |
937
|
|
|
'which are accessible to the CLI as "env" ' |
938
|
|
|
'parameter to the action. Note: Only works ' |
939
|
|
|
'with python, local and remote runners.') |
940
|
|
|
self.parser.add_argument('-u', '--user', type=str, default=None, |
941
|
|
|
help='User under which to run the action (admins only).') |
942
|
|
|
|
943
|
|
|
if self.name == 'run': |
944
|
|
|
self.parser.set_defaults(async=False) |
945
|
|
|
else: |
946
|
|
|
self.parser.set_defaults(async=True) |
947
|
|
|
|
948
|
|
|
@add_auth_token_to_kwargs_from_cli |
949
|
|
|
def run(self, args, **kwargs): |
950
|
|
|
if not args.ref_or_id: |
951
|
|
|
self.parser.error('Missing action reference or id') |
952
|
|
|
|
953
|
|
|
action = self.get_resource(args.ref_or_id, **kwargs) |
954
|
|
|
if not action: |
955
|
|
|
raise resource.ResourceNotFoundError('Action "%s" cannot be found.' |
956
|
|
|
% (args.ref_or_id)) |
957
|
|
|
|
958
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
959
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
960
|
|
|
if not runner: |
961
|
|
|
raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.' |
962
|
|
|
% (action.runner_type, action.name)) |
963
|
|
|
|
964
|
|
|
action_ref = '.'.join([action.pack, action.name]) |
965
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
966
|
|
|
args=args) |
967
|
|
|
|
968
|
|
|
execution = models.LiveAction() |
969
|
|
|
execution.action = action_ref |
970
|
|
|
execution.parameters = action_parameters |
971
|
|
|
execution.user = args.user |
972
|
|
|
|
973
|
|
|
if not args.trace_id and args.trace_tag: |
974
|
|
|
execution.context = {'trace_context': {'trace_tag': args.trace_tag}} |
975
|
|
|
|
976
|
|
|
if args.trace_id: |
977
|
|
|
execution.context = {'trace_context': {'id_': args.trace_id}} |
978
|
|
|
|
979
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
980
|
|
|
|
981
|
|
|
execution = action_exec_mgr.create(execution, **kwargs) |
982
|
|
|
execution = self._get_execution_result(execution=execution, |
983
|
|
|
action_exec_mgr=action_exec_mgr, |
984
|
|
|
args=args, **kwargs) |
985
|
|
|
return execution |
986
|
|
|
|
987
|
|
|
|
988
|
|
|
class ActionExecutionBranch(resource.ResourceBranch): |
989
|
|
|
|
990
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
991
|
|
|
super(ActionExecutionBranch, self).__init__( |
992
|
|
|
models.LiveAction, description, app, subparsers, |
993
|
|
|
parent_parser=parent_parser, read_only=True, |
994
|
|
|
commands={'list': ActionExecutionListCommand, |
995
|
|
|
'get': ActionExecutionGetCommand}) |
996
|
|
|
|
997
|
|
|
# Register extended commands |
998
|
|
|
self.commands['re-run'] = ActionExecutionReRunCommand( |
999
|
|
|
self.resource, self.app, self.subparsers, add_help=False) |
1000
|
|
|
self.commands['cancel'] = ActionExecutionCancelCommand( |
1001
|
|
|
self.resource, self.app, self.subparsers, add_help=True) |
1002
|
|
|
self.commands['pause'] = ActionExecutionPauseCommand( |
1003
|
|
|
self.resource, self.app, self.subparsers, add_help=True) |
1004
|
|
|
self.commands['resume'] = ActionExecutionResumeCommand( |
1005
|
|
|
self.resource, self.app, self.subparsers, add_help=True) |
1006
|
|
|
self.commands['tail'] = ActionExecutionTailCommand(self.resource, self.app, |
1007
|
|
|
self.subparsers, |
1008
|
|
|
add_help=True) |
1009
|
|
|
|
1010
|
|
|
|
1011
|
|
|
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceling', |
1012
|
|
|
'canceled') |
1013
|
|
|
|
1014
|
|
|
|
1015
|
|
|
class ActionExecutionReadCommand(resource.ResourceCommand): |
1016
|
|
|
""" |
1017
|
|
|
Base class for read / view commands (list and get). |
1018
|
|
|
""" |
1019
|
|
|
|
1020
|
|
|
@classmethod |
1021
|
|
|
def _get_exclude_attributes(cls, args): |
1022
|
|
|
""" |
1023
|
|
|
Retrieve a list of exclude attributes for particular command line arguments. |
1024
|
|
|
""" |
1025
|
|
|
exclude_attributes = [] |
1026
|
|
|
|
1027
|
|
|
result_included = False |
1028
|
|
|
trigger_instance_included = False |
1029
|
|
|
|
1030
|
|
|
for attr in args.attr: |
1031
|
|
|
# Note: We perform startswith check so we correctly detected child attribute properties |
1032
|
|
|
# (e.g. result, result.stdout, result.stderr, etc.) |
1033
|
|
|
if attr.startswith('result'): |
1034
|
|
|
result_included = True |
1035
|
|
|
|
1036
|
|
|
if attr.startswith('trigger_instance'): |
1037
|
|
|
trigger_instance_included = True |
1038
|
|
|
|
1039
|
|
|
if not result_included: |
1040
|
|
|
exclude_attributes.append('result') |
1041
|
|
|
if not trigger_instance_included: |
1042
|
|
|
exclude_attributes.append('trigger_instance') |
1043
|
|
|
|
1044
|
|
|
return exclude_attributes |
1045
|
|
|
|
1046
|
|
|
|
1047
|
|
|
class ActionExecutionListCommand(ActionExecutionReadCommand): |
1048
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp', |
1049
|
|
|
'end_timestamp'] |
1050
|
|
|
attribute_transform_functions = { |
1051
|
|
|
'start_timestamp': format_isodate_for_user_timezone, |
1052
|
|
|
'end_timestamp': format_isodate_for_user_timezone, |
1053
|
|
|
'parameters': format_parameters, |
1054
|
|
|
'status': format_status |
1055
|
|
|
} |
1056
|
|
|
|
1057
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1058
|
|
|
super(ActionExecutionListCommand, self).__init__( |
1059
|
|
|
resource, 'list', 'Get the list of the 50 most recent %s.' % |
1060
|
|
|
resource.get_plural_display_name().lower(), |
1061
|
|
|
*args, **kwargs) |
1062
|
|
|
|
1063
|
|
|
self.default_limit = 50 |
1064
|
|
|
self.resource_name = resource.get_plural_display_name().lower() |
1065
|
|
|
self.group = self.parser.add_argument_group() |
1066
|
|
|
self.parser.add_argument('-n', '--last', type=int, dest='last', |
1067
|
|
|
default=self.default_limit, |
1068
|
|
|
help=('List N most recent %s.' % self.resource_name)) |
1069
|
|
|
self.parser.add_argument('-s', '--sort', type=str, dest='sort_order', |
1070
|
|
|
default='descending', |
1071
|
|
|
help=('Sort %s by start timestamp, ' |
1072
|
|
|
'asc|ascending (earliest first) ' |
1073
|
|
|
'or desc|descending (latest first)' % self.resource_name)) |
1074
|
|
|
|
1075
|
|
|
# Filter options |
1076
|
|
|
self.group.add_argument('--action', help='Action reference to filter the list.') |
1077
|
|
|
self.group.add_argument('--status', help=('Only return executions with the provided status.' |
1078
|
|
|
' Possible values are \'%s\', \'%s\', \'%s\',' |
1079
|
|
|
'\'%s\', \'%s\' or \'%s\'' |
1080
|
|
|
'.' % POSSIBLE_ACTION_STATUS_VALUES)) |
1081
|
|
|
self.group.add_argument('--trigger_instance', |
1082
|
|
|
help='Trigger instance id to filter the list.') |
1083
|
|
|
self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt', |
1084
|
|
|
default=None, |
1085
|
|
|
help=('Only return executions with timestamp ' |
1086
|
|
|
'greater than the one provided. ' |
1087
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
1088
|
|
|
self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt', |
1089
|
|
|
default=None, |
1090
|
|
|
help=('Only return executions with timestamp ' |
1091
|
|
|
'lower than the one provided. ' |
1092
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
1093
|
|
|
self.parser.add_argument('-l', '--showall', action='store_true', |
1094
|
|
|
help='') |
1095
|
|
|
|
1096
|
|
|
# Display options |
1097
|
|
|
self.parser.add_argument('-a', '--attr', nargs='+', |
1098
|
|
|
default=self.display_attributes, |
1099
|
|
|
help=('List of attributes to include in the ' |
1100
|
|
|
'output. "all" will return all ' |
1101
|
|
|
'attributes.')) |
1102
|
|
|
self.parser.add_argument('-w', '--width', nargs='+', type=int, |
1103
|
|
|
default=None, |
1104
|
|
|
help=('Set the width of columns in output.')) |
1105
|
|
|
|
1106
|
|
|
@add_auth_token_to_kwargs_from_cli |
1107
|
|
|
def run(self, args, **kwargs): |
1108
|
|
|
# Filtering options |
1109
|
|
|
if args.action: |
1110
|
|
|
kwargs['action'] = args.action |
1111
|
|
|
if args.status: |
1112
|
|
|
kwargs['status'] = args.status |
1113
|
|
|
if args.trigger_instance: |
1114
|
|
|
kwargs['trigger_instance'] = args.trigger_instance |
1115
|
|
|
if not args.showall: |
1116
|
|
|
# null is the magic string that translates to does not exist. |
1117
|
|
|
kwargs['parent'] = 'null' |
1118
|
|
|
if args.timestamp_gt: |
1119
|
|
|
kwargs['timestamp_gt'] = args.timestamp_gt |
1120
|
|
|
if args.timestamp_lt: |
1121
|
|
|
kwargs['timestamp_lt'] = args.timestamp_lt |
1122
|
|
|
if args.sort_order: |
1123
|
|
|
if args.sort_order in ['asc', 'ascending']: |
1124
|
|
|
kwargs['sort_asc'] = True |
1125
|
|
|
elif args.sort_order in ['desc', 'descending']: |
1126
|
|
|
kwargs['sort_desc'] = True |
1127
|
|
|
|
1128
|
|
|
# We exclude "result" and "trigger_instance" attributes which can contain a lot of data |
1129
|
|
|
# since they are not displayed nor used which speeds the common operation substantially. |
1130
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
1131
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
1132
|
|
|
kwargs['exclude_attributes'] = exclude_attributes |
1133
|
|
|
|
1134
|
|
|
return self.manager.query_with_count(limit=args.last, **kwargs) |
1135
|
|
|
|
1136
|
|
|
def run_and_print(self, args, **kwargs): |
1137
|
|
|
|
1138
|
|
|
result, count = self.run(args, **kwargs) |
1139
|
|
|
instances = format_wf_instances(result) |
1140
|
|
|
|
1141
|
|
|
if args.json or args.yaml: |
1142
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
1143
|
|
|
attributes=args.attr, widths=args.width, |
1144
|
|
|
json=args.json, |
1145
|
|
|
yaml=args.yaml, |
1146
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
1147
|
|
|
|
1148
|
|
|
else: |
1149
|
|
|
# Include elapsed time for running executions |
1150
|
|
|
instances = format_execution_statuses(instances) |
1151
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
1152
|
|
|
attributes=args.attr, widths=args.width, |
1153
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
1154
|
|
|
|
1155
|
|
|
if args.last and count and count > args.last: |
1156
|
|
|
table.SingleRowTable.note_box(self.resource_name, args.last) |
1157
|
|
|
|
1158
|
|
|
|
1159
|
|
|
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1160
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1161
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1162
|
|
|
|
1163
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1164
|
|
|
super(ActionExecutionGetCommand, self).__init__( |
1165
|
|
|
resource, 'get', |
1166
|
|
|
'Get individual %s.' % resource.get_display_name().lower(), |
1167
|
|
|
*args, **kwargs) |
1168
|
|
|
|
1169
|
|
|
self.parser.add_argument('id', |
1170
|
|
|
help=('ID of the %s.' % |
1171
|
|
|
resource.get_display_name().lower())) |
1172
|
|
|
|
1173
|
|
|
self._add_common_options() |
1174
|
|
|
|
1175
|
|
|
@add_auth_token_to_kwargs_from_cli |
1176
|
|
|
def run(self, args, **kwargs): |
1177
|
|
|
# We exclude "result" and / or "trigger_instance" attribute if it's not explicitly |
1178
|
|
|
# requested by user either via "--attr" flag or by default. |
1179
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
1180
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
1181
|
|
|
|
1182
|
|
|
kwargs['params'] = {'exclude_attributes': exclude_attributes} |
1183
|
|
|
|
1184
|
|
|
execution = self.get_resource_by_id(id=args.id, **kwargs) |
1185
|
|
|
return execution |
1186
|
|
|
|
1187
|
|
|
@add_auth_token_to_kwargs_from_cli |
1188
|
|
|
def run_and_print(self, args, **kwargs): |
1189
|
|
|
try: |
1190
|
|
|
execution = self.run(args, **kwargs) |
1191
|
|
|
|
1192
|
|
|
if not args.json and not args.yaml: |
1193
|
|
|
# Include elapsed time for running executions |
1194
|
|
|
execution = format_execution_status(execution) |
1195
|
|
|
except resource.ResourceNotFoundError: |
1196
|
|
|
self.print_not_found(args.id) |
1197
|
|
|
raise ResourceNotFoundError('Execution with id %s not found.' % (args.id)) |
1198
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1199
|
|
|
|
1200
|
|
|
|
1201
|
|
|
class ActionExecutionCancelCommand(resource.ResourceCommand): |
1202
|
|
|
|
1203
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1204
|
|
|
super(ActionExecutionCancelCommand, self).__init__( |
1205
|
|
|
resource, 'cancel', 'Cancel %s.' % |
1206
|
|
|
resource.get_plural_display_name().lower(), |
1207
|
|
|
*args, **kwargs) |
1208
|
|
|
|
1209
|
|
|
self.parser.add_argument('ids', |
1210
|
|
|
nargs='+', |
1211
|
|
|
help=('IDs of the %ss to cancel.' % |
1212
|
|
|
resource.get_display_name().lower())) |
1213
|
|
|
|
1214
|
|
|
def run(self, args, **kwargs): |
1215
|
|
|
responses = [] |
1216
|
|
|
for execution_id in args.ids: |
1217
|
|
|
response = self.manager.delete_by_id(execution_id) |
1218
|
|
|
responses.append([execution_id, response]) |
1219
|
|
|
|
1220
|
|
|
return responses |
1221
|
|
|
|
1222
|
|
|
@add_auth_token_to_kwargs_from_cli |
1223
|
|
|
def run_and_print(self, args, **kwargs): |
1224
|
|
|
responses = self.run(args, **kwargs) |
1225
|
|
|
|
1226
|
|
|
for execution_id, response in responses: |
1227
|
|
|
self._print_result(execution_id=execution_id, response=response) |
1228
|
|
|
|
1229
|
|
|
def _print_result(self, execution_id, response): |
1230
|
|
|
if response and 'faultstring' in response: |
1231
|
|
|
message = response.get('faultstring', 'Cancellation requested for %s with id %s.' % |
1232
|
|
|
(self.resource.get_display_name().lower(), execution_id)) |
1233
|
|
|
|
1234
|
|
|
elif response: |
1235
|
|
|
message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(), |
1236
|
|
|
execution_id) |
1237
|
|
|
else: |
1238
|
|
|
message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(), |
1239
|
|
|
execution_id) |
1240
|
|
|
print(message) |
1241
|
|
|
|
1242
|
|
|
|
1243
|
|
|
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
1244
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1245
|
|
|
|
1246
|
|
|
super(ActionExecutionReRunCommand, self).__init__( |
1247
|
|
|
resource, kwargs.pop('name', 're-run'), |
1248
|
|
|
'Re-run a particular action.', |
1249
|
|
|
*args, **kwargs) |
1250
|
|
|
|
1251
|
|
|
self.parser.add_argument('id', nargs='?', |
1252
|
|
|
metavar='id', |
1253
|
|
|
help='ID of action execution to re-run ') |
1254
|
|
|
self.parser.add_argument('parameters', nargs='*', |
1255
|
|
|
help='List of keyword args, positional args, ' |
1256
|
|
|
'and optional args for the action.') |
1257
|
|
|
self.parser.add_argument('--tasks', nargs='*', |
1258
|
|
|
help='Name of the workflow tasks to re-run.') |
1259
|
|
|
self.parser.add_argument('--no-reset', dest='no_reset', nargs='*', |
1260
|
|
|
help='Name of the with-items tasks to not reset. This only ' |
1261
|
|
|
'applies to Mistral workflows. By default, all iterations ' |
1262
|
|
|
'for with-items tasks is rerun. If no reset, only failed ' |
1263
|
|
|
' iterations are rerun.') |
1264
|
|
|
self.parser.add_argument('-a', '--async', |
1265
|
|
|
action='store_true', dest='async', |
1266
|
|
|
help='Do not wait for action to finish.') |
1267
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
1268
|
|
|
action='store_true', dest='inherit_env', |
1269
|
|
|
help='Pass all the environment variables ' |
1270
|
|
|
'which are accessible to the CLI as "env" ' |
1271
|
|
|
'parameter to the action. Note: Only works ' |
1272
|
|
|
'with python, local and remote runners.') |
1273
|
|
|
self.parser.add_argument('-h', '--help', |
1274
|
|
|
action='store_true', dest='help', |
1275
|
|
|
help='Print usage for the given action.') |
1276
|
|
|
|
1277
|
|
|
self._add_common_options() |
1278
|
|
|
|
1279
|
|
|
@add_auth_token_to_kwargs_from_cli |
1280
|
|
|
def run(self, args, **kwargs): |
1281
|
|
|
existing_execution = self.manager.get_by_id(args.id, **kwargs) |
1282
|
|
|
|
1283
|
|
|
if not existing_execution: |
1284
|
|
|
raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' % |
1285
|
|
|
(args.id)) |
1286
|
|
|
|
1287
|
|
|
action_mgr = self.app.client.managers['Action'] |
1288
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
1289
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
1290
|
|
|
|
1291
|
|
|
action_ref = existing_execution.action['ref'] |
1292
|
|
|
action = action_mgr.get_by_ref_or_id(action_ref) |
1293
|
|
|
runner = runner_mgr.get_by_name(action.runner_type) |
1294
|
|
|
|
1295
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
1296
|
|
|
args=args) |
1297
|
|
|
|
1298
|
|
|
execution = action_exec_mgr.re_run(execution_id=args.id, |
1299
|
|
|
parameters=action_parameters, |
1300
|
|
|
tasks=args.tasks, |
1301
|
|
|
no_reset=args.no_reset, |
1302
|
|
|
**kwargs) |
1303
|
|
|
|
1304
|
|
|
execution = self._get_execution_result(execution=execution, |
1305
|
|
|
action_exec_mgr=action_exec_mgr, |
1306
|
|
|
args=args, **kwargs) |
1307
|
|
|
|
1308
|
|
|
return execution |
1309
|
|
|
|
1310
|
|
|
|
1311
|
|
|
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1312
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1313
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1314
|
|
|
|
1315
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1316
|
|
|
super(ActionExecutionPauseCommand, self).__init__( |
1317
|
|
|
resource, 'pause', 'Pause %s (workflow executions only).' % |
1318
|
|
|
resource.get_plural_display_name().lower(), |
1319
|
|
|
*args, **kwargs) |
1320
|
|
|
|
1321
|
|
|
self.parser.add_argument('id', nargs='+', |
1322
|
|
|
metavar='id', |
1323
|
|
|
help='ID of action execution to pause.') |
1324
|
|
|
|
1325
|
|
|
self._add_common_options() |
1326
|
|
|
|
1327
|
|
|
@add_auth_token_to_kwargs_from_cli |
1328
|
|
|
def run(self, args, **kwargs): |
1329
|
|
|
return self.manager.pause(args.id) |
1330
|
|
|
|
1331
|
|
|
@add_auth_token_to_kwargs_from_cli |
1332
|
|
|
def run_and_print(self, args, **kwargs): |
1333
|
|
|
try: |
1334
|
|
|
execution = self.run(args, **kwargs) |
1335
|
|
|
|
1336
|
|
|
if not args.json and not args.yaml: |
1337
|
|
|
# Include elapsed time for running executions |
1338
|
|
|
execution = format_execution_status(execution) |
1339
|
|
|
except resource.ResourceNotFoundError: |
1340
|
|
|
self.print_not_found(args.id) |
1341
|
|
|
raise ResourceNotFoundError('Execution with id %s not found.' % (args.id)) |
1342
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1343
|
|
|
|
1344
|
|
|
|
1345
|
|
|
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1346
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1347
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1348
|
|
|
|
1349
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1350
|
|
|
super(ActionExecutionResumeCommand, self).__init__( |
1351
|
|
|
resource, 'resume', 'Resume %s (workflow executions only).' % |
1352
|
|
|
resource.get_plural_display_name().lower(), |
1353
|
|
|
*args, **kwargs) |
1354
|
|
|
|
1355
|
|
|
self.parser.add_argument('id', nargs='+', |
1356
|
|
|
metavar='id', |
1357
|
|
|
help='ID of action execution to resume.') |
1358
|
|
|
|
1359
|
|
|
self._add_common_options() |
1360
|
|
|
|
1361
|
|
|
@add_auth_token_to_kwargs_from_cli |
1362
|
|
|
def run(self, args, **kwargs): |
1363
|
|
|
return self.manager.resume(args.id) |
1364
|
|
|
|
1365
|
|
|
@add_auth_token_to_kwargs_from_cli |
1366
|
|
|
def run_and_print(self, args, **kwargs): |
1367
|
|
|
try: |
1368
|
|
|
execution = self.run(args, **kwargs) |
1369
|
|
|
|
1370
|
|
|
if not args.json and not args.yaml: |
1371
|
|
|
# Include elapsed time for running executions |
1372
|
|
|
execution = format_execution_status(execution) |
1373
|
|
|
except resource.ResourceNotFoundError: |
1374
|
|
|
self.print_not_found(args.id) |
1375
|
|
|
raise ResourceNotFoundError('Execution %s not found.' % (args.id)) |
1376
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1377
|
|
|
|
1378
|
|
|
|
1379
|
|
|
class ActionExecutionTailCommand(resource.ResourceCommand): |
1380
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1381
|
|
|
super(ActionExecutionTailCommand, self).__init__( |
1382
|
|
|
resource, kwargs.pop('name', 'tail'), |
1383
|
|
|
'Tail output of a particular execution.', |
1384
|
|
|
*args, **kwargs) |
1385
|
|
|
|
1386
|
|
|
self.parser.add_argument('id', nargs='?', |
1387
|
|
|
metavar='id', |
1388
|
|
|
default='last', |
1389
|
|
|
help='ID of action execution to tail.') |
1390
|
|
|
self.parser.add_argument('--type', dest='output_type', action='store', |
1391
|
|
|
help=('Type of output to tail for. If not provided, ' |
1392
|
|
|
'defaults to all.')) |
1393
|
|
|
self.parser.add_argument('--include-metadata', dest='include_metadata', |
1394
|
|
|
action='store_true', |
1395
|
|
|
default=False, |
1396
|
|
|
help=('Include metadata (timestamp, output type) with the ' |
1397
|
|
|
'output.')) |
1398
|
|
|
|
1399
|
|
|
def run(self, args, **kwargs): |
1400
|
|
|
pass |
1401
|
|
|
|
1402
|
|
|
@add_auth_token_to_kwargs_from_cli |
1403
|
|
|
def run_and_print(self, args, **kwargs): |
1404
|
|
|
execution_id = args.id |
1405
|
|
|
output_type = getattr(args, 'output_type', None) |
1406
|
|
|
include_metadata = args.include_metadata |
1407
|
|
|
|
1408
|
|
|
# Special case for id "last" |
1409
|
|
|
if execution_id == 'last': |
1410
|
|
|
executions = self.manager.query(limit=1) |
1411
|
|
|
if executions: |
1412
|
|
|
execution = executions[0] |
1413
|
|
|
execution_id = execution.id |
1414
|
|
|
else: |
1415
|
|
|
print('No executions found in db.') |
1416
|
|
|
return |
1417
|
|
|
else: |
1418
|
|
|
execution = self.manager.get_by_id(execution_id, **kwargs) |
1419
|
|
|
|
1420
|
|
|
if not execution: |
1421
|
|
|
raise ResourceNotFoundError('Execution with id %s not found.' % (args.id)) |
1422
|
|
|
|
1423
|
|
|
execution_manager = self.manager |
1424
|
|
|
stream_manager = self.app.client.managers['Stream'] |
1425
|
|
|
ActionExecutionTailCommand.tail_execution(execution=execution, |
1426
|
|
|
execution_manager=execution_manager, |
1427
|
|
|
stream_manager=stream_manager, |
1428
|
|
|
output_type=output_type, |
1429
|
|
|
include_metadata=include_metadata, |
1430
|
|
|
**kwargs) |
1431
|
|
|
|
1432
|
|
|
@classmethod |
1433
|
|
|
def tail_execution(cls, execution_manager, stream_manager, execution, output_type=None, |
1434
|
|
|
include_metadata=False, **kwargs): |
1435
|
|
|
execution_id = str(execution.id) |
1436
|
|
|
# Note: For non-workflow actions child_execution_id always matches parent_execution_id so |
1437
|
|
|
# we don't need to do any other checks to determine if executions represents a workflow |
1438
|
|
|
# action |
1439
|
|
|
parent_execution_id = execution_id |
1440
|
|
|
|
1441
|
|
|
# Execution has already finished |
1442
|
|
|
if execution.status in LIVEACTION_COMPLETED_STATES: |
1443
|
|
|
output = execution_manager.get_output(execution_id=execution_id, |
1444
|
|
|
output_type=output_type) |
1445
|
|
|
print(output) |
1446
|
|
|
print('Execution %s has completed (status=%s).' % (execution_id, execution.status)) |
1447
|
|
|
return |
1448
|
|
|
|
1449
|
|
|
events = ['st2.execution__update', 'st2.execution.output__create'] |
1450
|
|
|
|
1451
|
|
|
for event in stream_manager.listen(events, **kwargs): |
1452
|
|
|
status = event.get('status', None) |
1453
|
|
|
is_execution_event = status is not None |
1454
|
|
|
|
1455
|
|
|
# NOTE: Right now only a single level deep / nested workflows are supported |
1456
|
|
|
if is_execution_event: |
1457
|
|
|
context = cls.get_normalized_context_execution_task_event(event=event) |
1458
|
|
|
task_execution_id = context['execution_id'] |
1459
|
|
|
task_name = context['task_name'] |
1460
|
|
|
task_parent_execution_id = context['parent_execution_id'] |
1461
|
|
|
is_child_execution = (task_parent_execution_id == parent_execution_id) |
1462
|
|
|
|
1463
|
|
|
if is_child_execution: |
1464
|
|
|
if status == LIVEACTION_STATUS_RUNNING: |
1465
|
|
|
print('Child execution (task=%s) %s has started.' % (task_name, |
1466
|
|
|
task_execution_id)) |
1467
|
|
|
print('') |
1468
|
|
|
continue |
1469
|
|
|
elif status in LIVEACTION_COMPLETED_STATES: |
1470
|
|
|
print('') |
1471
|
|
|
print('Child execution (task=%s) %s has finished (status=%s).' % (task_name, |
1472
|
|
|
task_execution_id, status)) |
1473
|
|
|
continue |
1474
|
|
|
else: |
1475
|
|
|
# We don't care about other child events so we simply skip then |
1476
|
|
|
continue |
1477
|
|
|
else: |
1478
|
|
|
if status in LIVEACTION_COMPLETED_STATES: |
1479
|
|
|
# Bail out once parent execution has finished |
1480
|
|
|
print('') |
1481
|
|
|
print('Execution %s has completed (status=%s).' % (execution_id, status)) |
1482
|
|
|
break |
1483
|
|
|
else: |
1484
|
|
|
# We don't care about other execution events |
1485
|
|
|
continue |
1486
|
|
|
|
1487
|
|
|
# Filter on output_type if provided |
1488
|
|
|
event_output_type = event.get('output_type', None) |
1489
|
|
|
if output_type != 'all' and output_type and (event_output_type != output_type): |
1490
|
|
|
continue |
1491
|
|
|
|
1492
|
|
|
if include_metadata: |
1493
|
|
|
sys.stdout.write('[%s][%s] %s' % (event['timestamp'], event['output_type'], |
1494
|
|
|
event['data'])) |
1495
|
|
|
else: |
1496
|
|
|
sys.stdout.write(event['data']) |
1497
|
|
|
|
1498
|
|
|
@classmethod |
1499
|
|
|
def get_normalized_context_execution_task_event(cls, event): |
1500
|
|
|
""" |
1501
|
|
|
Return a dictionary with normalized context attributes for Action-Chain and Mistral |
1502
|
|
|
workflows. |
1503
|
|
|
""" |
1504
|
|
|
context = event.get('context', {}) |
1505
|
|
|
|
1506
|
|
|
result = { |
1507
|
|
|
'parent_execution_id': None, |
1508
|
|
|
'execution_id': None, |
1509
|
|
|
'task_name': None |
1510
|
|
|
} |
1511
|
|
|
|
1512
|
|
|
if 'mistral' in context: |
1513
|
|
|
# Mistral workflow |
1514
|
|
|
result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None) |
1515
|
|
|
result['execution_id'] = event['id'] |
1516
|
|
|
result['task_name'] = context.get('mistral', {}).get('task_name', 'unknown') |
1517
|
|
|
else: |
1518
|
|
|
# Action chain workflow |
1519
|
|
|
result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None) |
1520
|
|
|
result['execution_id'] = event['id'] |
1521
|
|
|
result['task_name'] = context.get('chain', {}).get('name', 'unknown') |
1522
|
|
|
|
1523
|
|
|
return result |
1524
|
|
|
|
It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior: