|
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 add_auth_token_to_kwargs_from_cli |
|
32
|
|
|
from st2client.exceptions.operations import OperationFailureException |
|
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 |
|
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
|
|
|
|
|
53
|
|
|
|
|
54
|
|
|
LIVEACTION_COMPLETED_STATES = [ |
|
55
|
|
|
LIVEACTION_STATUS_SUCCEEDED, |
|
56
|
|
|
LIVEACTION_STATUS_FAILED, |
|
57
|
|
|
LIVEACTION_STATUS_TIMED_OUT, |
|
58
|
|
|
LIVEACTION_STATUS_CANCELED, |
|
59
|
|
|
LIVEACTION_STATUS_ABANDONED |
|
60
|
|
|
] |
|
61
|
|
|
|
|
62
|
|
|
# Who parameters should be masked when displaying action execution output |
|
63
|
|
|
PARAMETERS_TO_MASK = [ |
|
64
|
|
|
'password', |
|
65
|
|
|
'private_key' |
|
66
|
|
|
] |
|
67
|
|
|
|
|
68
|
|
|
# A list of environment variables which are never inherited when using run |
|
69
|
|
|
# --inherit-env flag |
|
70
|
|
|
ENV_VARS_BLACKLIST = [ |
|
71
|
|
|
'pwd', |
|
72
|
|
|
'mail', |
|
73
|
|
|
'username', |
|
74
|
|
|
'user', |
|
75
|
|
|
'path', |
|
76
|
|
|
'home', |
|
77
|
|
|
'ps1', |
|
78
|
|
|
'shell', |
|
79
|
|
|
'pythonpath', |
|
80
|
|
|
'ssh_tty', |
|
81
|
|
|
'ssh_connection', |
|
82
|
|
|
'lang', |
|
83
|
|
|
'ls_colors', |
|
84
|
|
|
'logname', |
|
85
|
|
|
'oldpwd', |
|
86
|
|
|
'term', |
|
87
|
|
|
'xdg_session_id' |
|
88
|
|
|
] |
|
89
|
|
|
|
|
90
|
|
|
WORKFLOW_RUNNER_TYPES = [ |
|
91
|
|
|
'action-chain', |
|
92
|
|
|
'mistral-v2', |
|
93
|
|
|
] |
|
94
|
|
|
|
|
95
|
|
|
|
|
96
|
|
|
def format_parameters(value): |
|
97
|
|
|
# Mask sensitive parameters |
|
98
|
|
|
if not isinstance(value, dict): |
|
99
|
|
|
# No parameters, leave it as it is |
|
100
|
|
|
return value |
|
101
|
|
|
|
|
102
|
|
|
for param_name, _ in value.items(): |
|
103
|
|
|
if param_name in PARAMETERS_TO_MASK: |
|
104
|
|
|
value[param_name] = '********' |
|
105
|
|
|
|
|
106
|
|
|
return value |
|
107
|
|
|
|
|
108
|
|
|
# String for indenting etc. |
|
109
|
|
|
WF_PREFIX = '+ ' |
|
110
|
|
|
NON_WF_PREFIX = ' ' |
|
111
|
|
|
INDENT_CHAR = ' ' |
|
112
|
|
|
|
|
113
|
|
|
|
|
114
|
|
|
def format_wf_instances(instances): |
|
115
|
|
|
""" |
|
116
|
|
|
Adds identification characters to a workflow and appropriately shifts |
|
117
|
|
|
the non-workflow instances. If no workflows are found does nothing. |
|
118
|
|
|
""" |
|
119
|
|
|
# only add extr chars if there are workflows. |
|
120
|
|
|
has_wf = False |
|
121
|
|
|
for instance in instances: |
|
122
|
|
|
if not getattr(instance, 'children', None): |
|
123
|
|
|
continue |
|
124
|
|
|
else: |
|
125
|
|
|
has_wf = True |
|
126
|
|
|
break |
|
127
|
|
|
if not has_wf: |
|
128
|
|
|
return instances |
|
129
|
|
|
# Prepend wf and non_wf prefixes. |
|
130
|
|
|
for instance in instances: |
|
131
|
|
|
if getattr(instance, 'children', None): |
|
132
|
|
|
instance.id = WF_PREFIX + instance.id |
|
133
|
|
|
else: |
|
134
|
|
|
instance.id = NON_WF_PREFIX + instance.id |
|
135
|
|
|
return instances |
|
136
|
|
|
|
|
137
|
|
|
|
|
138
|
|
|
def format_execution_statuses(instances): |
|
139
|
|
|
result = [] |
|
140
|
|
|
for instance in instances: |
|
141
|
|
|
instance = format_execution_status(instance) |
|
142
|
|
|
result.append(instance) |
|
143
|
|
|
|
|
144
|
|
|
return result |
|
145
|
|
|
|
|
146
|
|
|
|
|
147
|
|
|
def format_execution_status(instance): |
|
148
|
|
|
""" |
|
149
|
|
|
Augment instance "status" attribute with number of seconds which have elapsed for all the |
|
150
|
|
|
executions which are in running state and execution total run time for all the executions |
|
151
|
|
|
which have finished. |
|
152
|
|
|
""" |
|
153
|
|
|
start_timestamp = getattr(instance, 'start_timestamp', None) |
|
154
|
|
|
end_timestamp = getattr(instance, 'end_timestamp', None) |
|
155
|
|
|
|
|
156
|
|
|
if instance.status == LIVEACTION_STATUS_RUNNING and start_timestamp: |
|
157
|
|
|
start_timestamp = instance.start_timestamp |
|
158
|
|
|
start_timestamp = parse_isotime(start_timestamp) |
|
159
|
|
|
start_timestamp = calendar.timegm(start_timestamp.timetuple()) |
|
160
|
|
|
now = int(time.time()) |
|
161
|
|
|
elapsed_seconds = (now - start_timestamp) |
|
162
|
|
|
instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds) |
|
163
|
|
|
elif instance.status in LIVEACTION_COMPLETED_STATES and start_timestamp and end_timestamp: |
|
164
|
|
|
start_timestamp = parse_isotime(start_timestamp) |
|
165
|
|
|
start_timestamp = calendar.timegm(start_timestamp.timetuple()) |
|
166
|
|
|
end_timestamp = parse_isotime(end_timestamp) |
|
167
|
|
|
end_timestamp = calendar.timegm(end_timestamp.timetuple()) |
|
168
|
|
|
|
|
169
|
|
|
elapsed_seconds = (end_timestamp - start_timestamp) |
|
170
|
|
|
instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds) |
|
171
|
|
|
|
|
172
|
|
|
return instance |
|
173
|
|
|
|
|
174
|
|
|
|
|
175
|
|
|
class ActionBranch(resource.ResourceBranch): |
|
176
|
|
|
|
|
177
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
|
178
|
|
|
super(ActionBranch, self).__init__( |
|
179
|
|
|
models.Action, description, app, subparsers, |
|
180
|
|
|
parent_parser=parent_parser, |
|
181
|
|
|
commands={ |
|
182
|
|
|
'list': ActionListCommand, |
|
183
|
|
|
'get': ActionGetCommand, |
|
184
|
|
|
'update': ActionUpdateCommand, |
|
185
|
|
|
'delete': ActionDeleteCommand |
|
186
|
|
|
}) |
|
187
|
|
|
|
|
188
|
|
|
# Registers extended commands |
|
189
|
|
|
self.commands['enable'] = ActionEnableCommand(self.resource, self.app, self.subparsers) |
|
190
|
|
|
self.commands['disable'] = ActionDisableCommand(self.resource, self.app, self.subparsers) |
|
191
|
|
|
self.commands['execute'] = ActionRunCommand( |
|
192
|
|
|
self.resource, self.app, self.subparsers, |
|
193
|
|
|
add_help=False) |
|
194
|
|
|
|
|
195
|
|
|
|
|
196
|
|
|
class ActionListCommand(resource.ContentPackResourceListCommand): |
|
197
|
|
|
display_attributes = ['ref', 'pack', 'description'] |
|
198
|
|
|
|
|
199
|
|
|
|
|
200
|
|
|
class ActionGetCommand(resource.ContentPackResourceGetCommand): |
|
201
|
|
|
display_attributes = ['all'] |
|
202
|
|
|
attribute_display_order = ['id', 'uid', 'ref', 'pack', 'name', 'description', |
|
203
|
|
|
'enabled', 'entry_point', 'runner_type', |
|
204
|
|
|
'parameters'] |
|
205
|
|
|
|
|
206
|
|
|
|
|
207
|
|
|
class ActionUpdateCommand(resource.ContentPackResourceUpdateCommand): |
|
208
|
|
|
pass |
|
209
|
|
|
|
|
210
|
|
|
|
|
211
|
|
|
class ActionEnableCommand(resource.ContentPackResourceEnableCommand): |
|
212
|
|
|
display_attributes = ['all'] |
|
213
|
|
|
attribute_display_order = ['id', 'ref', 'pack', 'name', 'description', |
|
214
|
|
|
'enabled', 'entry_point', 'runner_type', |
|
215
|
|
|
'parameters'] |
|
216
|
|
|
|
|
217
|
|
|
|
|
218
|
|
|
class ActionDisableCommand(resource.ContentPackResourceDisableCommand): |
|
219
|
|
|
display_attributes = ['all'] |
|
220
|
|
|
attribute_display_order = ['id', 'ref', 'pack', 'name', 'description', |
|
221
|
|
|
'enabled', 'entry_point', 'runner_type', |
|
222
|
|
|
'parameters'] |
|
223
|
|
|
|
|
224
|
|
|
|
|
225
|
|
|
class ActionDeleteCommand(resource.ContentPackResourceDeleteCommand): |
|
226
|
|
|
pass |
|
227
|
|
|
|
|
228
|
|
|
|
|
229
|
|
|
class ActionRunCommandMixin(object): |
|
230
|
|
|
""" |
|
231
|
|
|
Mixin class which contains utility functions related to action execution. |
|
232
|
|
|
""" |
|
233
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
|
234
|
|
|
'start_timestamp', 'end_timestamp', 'result'] |
|
235
|
|
|
attribute_display_order = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
|
236
|
|
|
'start_timestamp', 'end_timestamp', 'result'] |
|
237
|
|
|
attribute_transform_functions = { |
|
238
|
|
|
'start_timestamp': format_isodate, |
|
239
|
|
|
'end_timestamp': format_isodate, |
|
240
|
|
|
'parameters': format_parameters, |
|
241
|
|
|
'status': format_status |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
poll_interval = 2 # how often to poll for execution completion when using sync mode |
|
245
|
|
|
|
|
246
|
|
|
def get_resource(self, ref_or_id, **kwargs): |
|
247
|
|
|
return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs) |
|
248
|
|
|
|
|
249
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
250
|
|
|
def run_and_print(self, args, **kwargs): |
|
251
|
|
|
if self._print_help(args, **kwargs): |
|
252
|
|
|
return |
|
253
|
|
|
|
|
254
|
|
|
execution = self.run(args, **kwargs) |
|
255
|
|
|
if args.async: |
|
256
|
|
|
self.print_output('To get the results, execute:\n st2 execution get %s' % |
|
257
|
|
|
(execution.id), six.text_type) |
|
258
|
|
|
else: |
|
259
|
|
|
self._print_execution_details(execution=execution, args=args, **kwargs) |
|
260
|
|
|
|
|
261
|
|
|
if execution.status == 'failed': |
|
262
|
|
|
# Exit with non zero if the action has failed |
|
263
|
|
|
sys.exit(1) |
|
264
|
|
|
|
|
265
|
|
|
def _add_common_options(self): |
|
266
|
|
|
root_arg_grp = self.parser.add_mutually_exclusive_group() |
|
267
|
|
|
|
|
268
|
|
|
# Display options |
|
269
|
|
|
task_list_arg_grp = root_arg_grp.add_argument_group() |
|
270
|
|
|
task_list_arg_grp.add_argument('--raw', action='store_true', |
|
271
|
|
|
help='Raw output, don\'t shot sub-tasks for workflows.') |
|
272
|
|
|
task_list_arg_grp.add_argument('--show-tasks', action='store_true', |
|
273
|
|
|
help='Whether to show sub-tasks of an execution.') |
|
274
|
|
|
task_list_arg_grp.add_argument('--depth', type=int, default=-1, |
|
275
|
|
|
help='Depth to which to show sub-tasks. \ |
|
276
|
|
|
By default all are shown.') |
|
277
|
|
|
task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None, |
|
278
|
|
|
help='Set the width of columns in output.') |
|
279
|
|
|
|
|
280
|
|
|
execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group() |
|
281
|
|
|
|
|
282
|
|
|
detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
|
283
|
|
|
detail_arg_grp.add_argument('--attr', nargs='+', |
|
284
|
|
|
default=['id', 'status', 'parameters', 'result'], |
|
285
|
|
|
help=('List of attributes to include in the ' |
|
286
|
|
|
'output. "all" or unspecified will ' |
|
287
|
|
|
'return all attributes.')) |
|
288
|
|
|
detail_arg_grp.add_argument('-d', '--detail', action='store_true', |
|
289
|
|
|
help='Display full detail of the execution in table format.') |
|
290
|
|
|
|
|
291
|
|
|
result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
|
292
|
|
|
result_arg_grp.add_argument('-k', '--key', |
|
293
|
|
|
help=('If result is type of JSON, then print specific ' |
|
294
|
|
|
'key-value pair; dot notation for nested JSON is ' |
|
295
|
|
|
'supported.')) |
|
296
|
|
|
|
|
297
|
|
|
return root_arg_grp |
|
298
|
|
|
|
|
299
|
|
|
def _print_execution_details(self, execution, args, **kwargs): |
|
300
|
|
|
""" |
|
301
|
|
|
Print the execution detail to stdout. |
|
302
|
|
|
|
|
303
|
|
|
This method takes into account if an executed action was workflow or not |
|
304
|
|
|
and formats the output accordingly. |
|
305
|
|
|
""" |
|
306
|
|
|
runner_type = execution.action.get('runner_type', 'unknown') |
|
307
|
|
|
is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES |
|
308
|
|
|
|
|
309
|
|
|
show_tasks = getattr(args, 'show_tasks', False) |
|
310
|
|
|
raw = getattr(args, 'raw', False) |
|
311
|
|
|
detail = getattr(args, 'detail', False) |
|
312
|
|
|
key = getattr(args, 'key', None) |
|
313
|
|
|
attr = getattr(args, 'attr', []) |
|
314
|
|
|
|
|
315
|
|
|
if show_tasks and not is_workflow_action: |
|
316
|
|
|
raise ValueError('--show-tasks option can only be used with workflow actions') |
|
317
|
|
|
|
|
318
|
|
|
if not raw and not detail and (show_tasks or is_workflow_action): |
|
319
|
|
|
self._run_and_print_child_task_list(execution=execution, args=args, **kwargs) |
|
320
|
|
|
else: |
|
321
|
|
|
instance = execution |
|
322
|
|
|
|
|
323
|
|
|
if detail: |
|
324
|
|
|
formatter = table.PropertyValueTable |
|
325
|
|
|
else: |
|
326
|
|
|
formatter = execution_formatter.ExecutionResult |
|
327
|
|
|
|
|
328
|
|
|
if detail: |
|
329
|
|
|
options = {'attributes': copy.copy(self.display_attributes)} |
|
330
|
|
|
elif key: |
|
331
|
|
|
options = {'attributes': ['result.%s' % (key)], 'key': key} |
|
332
|
|
|
else: |
|
333
|
|
|
options = {'attributes': attr} |
|
334
|
|
|
|
|
335
|
|
|
options['json'] = args.json |
|
336
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
|
337
|
|
|
self.print_output(instance, formatter, **options) |
|
338
|
|
|
|
|
339
|
|
|
def _run_and_print_child_task_list(self, execution, args, **kwargs): |
|
340
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
|
341
|
|
|
|
|
342
|
|
|
instance = execution |
|
343
|
|
|
options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp', |
|
344
|
|
|
'end_timestamp']} |
|
345
|
|
|
options['json'] = args.json |
|
346
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
|
347
|
|
|
formatter = execution_formatter.ExecutionResult |
|
348
|
|
|
|
|
349
|
|
|
kwargs['depth'] = args.depth |
|
350
|
|
|
child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs) |
|
351
|
|
|
child_instances = self._format_child_instances(child_instances, execution.id) |
|
352
|
|
|
child_instances = format_execution_statuses(child_instances) |
|
353
|
|
|
|
|
354
|
|
|
if not child_instances: |
|
355
|
|
|
# No child error, there might be a global error, include result in the output |
|
356
|
|
|
options['attributes'].append('result') |
|
357
|
|
|
|
|
358
|
|
|
# On failure we also want to include error message and traceback at the top level |
|
359
|
|
|
if instance.status == 'failed': |
|
360
|
|
|
status_index = options['attributes'].index('status') |
|
361
|
|
|
if isinstance(instance.result, dict): |
|
362
|
|
|
tasks = instance.result.get('tasks', []) |
|
363
|
|
|
else: |
|
364
|
|
|
tasks = [] |
|
365
|
|
|
|
|
366
|
|
|
top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance) |
|
367
|
|
|
|
|
368
|
|
|
if len(tasks) >= 1: |
|
369
|
|
|
task_error, task_traceback = self._get_task_error(task=tasks[-1]) |
|
370
|
|
|
else: |
|
371
|
|
|
task_error, task_traceback = None, None |
|
372
|
|
|
|
|
373
|
|
|
if top_level_error: |
|
374
|
|
|
# Top-level error |
|
375
|
|
|
instance.error = top_level_error |
|
376
|
|
|
instance.traceback = top_level_traceback |
|
377
|
|
|
instance.result = 'See error and traceback.' |
|
378
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
|
379
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
|
380
|
|
|
elif task_error: |
|
381
|
|
|
# Task error |
|
382
|
|
|
instance.error = task_error |
|
383
|
|
|
instance.traceback = task_traceback |
|
384
|
|
|
instance.result = 'See error and traceback.' |
|
385
|
|
|
instance.failed_on = tasks[-1].get('name', 'unknown') |
|
386
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
|
387
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
|
388
|
|
|
options['attributes'].insert(status_index + 3, 'failed_on') |
|
389
|
|
|
|
|
390
|
|
|
# print root task |
|
391
|
|
|
self.print_output(instance, formatter, **options) |
|
392
|
|
|
|
|
393
|
|
|
# print child tasks |
|
394
|
|
|
if child_instances: |
|
395
|
|
|
self.print_output(child_instances, table.MultiColumnTable, |
|
396
|
|
|
attributes=['id', 'status', 'task', 'action', 'start_timestamp'], |
|
397
|
|
|
widths=args.width, json=args.json, |
|
398
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
|
399
|
|
|
|
|
400
|
|
|
def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): |
|
401
|
|
|
pending_statuses = [ |
|
402
|
|
|
LIVEACTION_STATUS_REQUESTED, |
|
403
|
|
|
LIVEACTION_STATUS_SCHEDULED, |
|
404
|
|
|
LIVEACTION_STATUS_RUNNING, |
|
405
|
|
|
LIVEACTION_STATUS_CANCELING |
|
406
|
|
|
] |
|
407
|
|
|
|
|
408
|
|
|
if not args.async: |
|
409
|
|
|
while execution.status in pending_statuses: |
|
410
|
|
|
time.sleep(self.poll_interval) |
|
411
|
|
|
if not args.json: |
|
412
|
|
|
sys.stdout.write('.') |
|
413
|
|
|
sys.stdout.flush() |
|
414
|
|
|
execution = action_exec_mgr.get_by_id(execution.id, **kwargs) |
|
415
|
|
|
|
|
416
|
|
|
sys.stdout.write('\n') |
|
417
|
|
|
|
|
418
|
|
|
if execution.status == LIVEACTION_STATUS_CANCELED: |
|
419
|
|
|
return execution |
|
420
|
|
|
|
|
421
|
|
|
return execution |
|
422
|
|
|
|
|
423
|
|
|
def _get_top_level_error(self, live_action): |
|
424
|
|
|
""" |
|
425
|
|
|
Retrieve a top level workflow error. |
|
426
|
|
|
|
|
427
|
|
|
:return: (error, traceback) |
|
428
|
|
|
""" |
|
429
|
|
|
if isinstance(live_action.result, dict): |
|
430
|
|
|
error = live_action.result.get('error', None) |
|
431
|
|
|
traceback = live_action.result.get('traceback', None) |
|
432
|
|
|
else: |
|
433
|
|
|
error = "See result" |
|
434
|
|
|
traceback = "See result" |
|
435
|
|
|
|
|
436
|
|
|
return error, traceback |
|
437
|
|
|
|
|
438
|
|
|
def _get_task_error(self, task): |
|
439
|
|
|
""" |
|
440
|
|
|
Retrieve error message from the provided task. |
|
441
|
|
|
|
|
442
|
|
|
:return: (error, traceback) |
|
443
|
|
|
""" |
|
444
|
|
|
if not task: |
|
445
|
|
|
return None, None |
|
446
|
|
|
|
|
447
|
|
|
result = task['result'] |
|
448
|
|
|
|
|
449
|
|
|
if isinstance(result, dict): |
|
450
|
|
|
stderr = result.get('stderr', None) |
|
451
|
|
|
error = result.get('error', None) |
|
452
|
|
|
traceback = result.get('traceback', None) |
|
453
|
|
|
error = error if error else stderr |
|
454
|
|
|
else: |
|
455
|
|
|
stderr = None |
|
456
|
|
|
error = None |
|
457
|
|
|
traceback = None |
|
458
|
|
|
|
|
459
|
|
|
return error, traceback |
|
460
|
|
|
|
|
461
|
|
|
def _get_action_parameters_from_args(self, action, runner, args): |
|
462
|
|
|
""" |
|
463
|
|
|
Build a dictionary with parameters which will be passed to the action by |
|
464
|
|
|
parsing parameters passed to the CLI. |
|
465
|
|
|
|
|
466
|
|
|
:param args: CLI argument. |
|
467
|
|
|
:type args: ``object`` |
|
468
|
|
|
|
|
469
|
|
|
:rtype: ``dict`` |
|
470
|
|
|
""" |
|
471
|
|
|
action_ref_or_id = action.ref |
|
472
|
|
|
|
|
473
|
|
|
def read_file(file_path): |
|
474
|
|
|
if not os.path.exists(file_path): |
|
475
|
|
|
raise ValueError('File "%s" doesn\'t exist' % (file_path)) |
|
476
|
|
|
|
|
477
|
|
|
if not os.path.isfile(file_path): |
|
478
|
|
|
raise ValueError('"%s" is not a file' % (file_path)) |
|
479
|
|
|
|
|
480
|
|
|
with open(file_path, 'rb') as fp: |
|
481
|
|
|
content = fp.read() |
|
482
|
|
|
|
|
483
|
|
|
return content |
|
484
|
|
|
|
|
485
|
|
|
def transform_object(value): |
|
486
|
|
|
# Also support simple key1=val1,key2=val2 syntax |
|
487
|
|
|
if value.startswith('{'): |
|
488
|
|
|
# Assume it's JSON |
|
489
|
|
|
result = value = json.loads(value) |
|
490
|
|
|
else: |
|
491
|
|
|
pairs = value.split(',') |
|
492
|
|
|
|
|
493
|
|
|
result = {} |
|
494
|
|
|
for pair in pairs: |
|
495
|
|
|
split = pair.split('=', 1) |
|
496
|
|
|
|
|
497
|
|
|
if len(split) != 2: |
|
498
|
|
|
continue |
|
499
|
|
|
|
|
500
|
|
|
key, value = split |
|
501
|
|
|
result[key] = value |
|
502
|
|
|
return result |
|
503
|
|
|
|
|
504
|
|
|
transformer = { |
|
505
|
|
|
'array': (lambda cs_x: [v.strip() for v in cs_x.split(',')]), |
|
506
|
|
|
'boolean': (lambda x: ast.literal_eval(x.capitalize())), |
|
507
|
|
|
'integer': int, |
|
508
|
|
|
'number': float, |
|
509
|
|
|
'object': transform_object, |
|
510
|
|
|
'string': str |
|
511
|
|
|
} |
|
512
|
|
|
|
|
513
|
|
|
def normalize(name, value): |
|
514
|
|
|
if name in runner.runner_parameters: |
|
515
|
|
|
param = runner.runner_parameters[name] |
|
516
|
|
|
if 'type' in param and param['type'] in transformer: |
|
517
|
|
|
return transformer[param['type']](value) |
|
518
|
|
|
|
|
519
|
|
|
if name in action.parameters: |
|
520
|
|
|
param = action.parameters[name] |
|
521
|
|
|
if 'type' in param and param['type'] in transformer: |
|
522
|
|
|
return transformer[param['type']](value) |
|
523
|
|
|
return value |
|
524
|
|
|
|
|
525
|
|
|
result = {} |
|
526
|
|
|
|
|
527
|
|
|
if not args.parameters: |
|
528
|
|
|
return result |
|
529
|
|
|
|
|
530
|
|
|
for idx in range(len(args.parameters)): |
|
531
|
|
|
arg = args.parameters[idx] |
|
532
|
|
|
if '=' in arg: |
|
533
|
|
|
k, v = arg.split('=', 1) |
|
534
|
|
|
|
|
535
|
|
|
# Attribute for files are prefixed with "@" |
|
536
|
|
|
if k.startswith('@'): |
|
537
|
|
|
k = k[1:] |
|
538
|
|
|
is_file = True |
|
539
|
|
|
else: |
|
540
|
|
|
is_file = False |
|
541
|
|
|
|
|
542
|
|
|
try: |
|
543
|
|
|
if is_file: |
|
544
|
|
|
# Files are handled a bit differently since we ship the content |
|
545
|
|
|
# over the wire |
|
546
|
|
|
file_path = os.path.normpath(pjoin(os.getcwd(), v)) |
|
547
|
|
|
file_name = os.path.basename(file_path) |
|
548
|
|
|
content = read_file(file_path=file_path) |
|
549
|
|
|
|
|
550
|
|
|
if action_ref_or_id == 'core.http': |
|
551
|
|
|
# Special case for http runner |
|
552
|
|
|
result['_file_name'] = file_name |
|
553
|
|
|
result['file_content'] = content |
|
554
|
|
|
else: |
|
555
|
|
|
result[k] = content |
|
556
|
|
|
else: |
|
557
|
|
|
result[k] = normalize(k, v) |
|
558
|
|
|
except Exception as e: |
|
559
|
|
|
# TODO: Move transformers in a separate module and handle |
|
560
|
|
|
# exceptions there |
|
561
|
|
|
if 'malformed string' in str(e): |
|
562
|
|
|
message = ('Invalid value for boolean parameter. ' |
|
563
|
|
|
'Valid values are: true, false') |
|
564
|
|
|
raise ValueError(message) |
|
565
|
|
|
else: |
|
566
|
|
|
raise e |
|
567
|
|
|
else: |
|
568
|
|
|
result['cmd'] = ' '.join(args.parameters[idx:]) |
|
569
|
|
|
break |
|
570
|
|
|
|
|
571
|
|
|
# Special case for http runner |
|
572
|
|
|
if 'file_content' in result: |
|
573
|
|
|
if 'method' not in result: |
|
574
|
|
|
# Default to POST if a method is not provided |
|
575
|
|
|
result['method'] = 'POST' |
|
576
|
|
|
|
|
577
|
|
|
if 'file_name' not in result: |
|
578
|
|
|
# File name not provided, use default file name |
|
579
|
|
|
result['file_name'] = result['_file_name'] |
|
580
|
|
|
|
|
581
|
|
|
del result['_file_name'] |
|
582
|
|
|
|
|
583
|
|
|
if args.inherit_env: |
|
584
|
|
|
result['env'] = self._get_inherited_env_vars() |
|
585
|
|
|
|
|
586
|
|
|
return result |
|
587
|
|
|
|
|
588
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
589
|
|
|
def _print_help(self, args, **kwargs): |
|
590
|
|
|
# Print appropriate help message if the help option is given. |
|
591
|
|
|
action_mgr = self.app.client.managers['Action'] |
|
592
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
|
593
|
|
|
|
|
594
|
|
|
if args.help: |
|
595
|
|
|
action_ref_or_id = getattr(args, 'ref_or_id', None) |
|
596
|
|
|
action_exec_id = getattr(args, 'id', None) |
|
597
|
|
|
|
|
598
|
|
|
if action_exec_id and not action_ref_or_id: |
|
599
|
|
|
action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs) |
|
600
|
|
|
args.ref_or_id = action_exec.action |
|
601
|
|
|
|
|
602
|
|
|
if action_ref_or_id: |
|
603
|
|
|
try: |
|
604
|
|
|
action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs) |
|
605
|
|
|
if not action: |
|
606
|
|
|
raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id) |
|
607
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
|
608
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
|
609
|
|
|
parameters, required, optional, _ = self._get_params_types(runner, |
|
610
|
|
|
action) |
|
611
|
|
|
print('') |
|
612
|
|
|
print(textwrap.fill(action.description)) |
|
613
|
|
|
print('') |
|
614
|
|
|
if required: |
|
615
|
|
|
required = self._sort_parameters(parameters=parameters, |
|
616
|
|
|
names=required) |
|
617
|
|
|
|
|
618
|
|
|
print('Required Parameters:') |
|
619
|
|
|
[self._print_param(name, parameters.get(name)) |
|
620
|
|
|
for name in required] |
|
621
|
|
|
if optional: |
|
622
|
|
|
optional = self._sort_parameters(parameters=parameters, |
|
623
|
|
|
names=optional) |
|
624
|
|
|
|
|
625
|
|
|
print('Optional Parameters:') |
|
626
|
|
|
[self._print_param(name, parameters.get(name)) |
|
627
|
|
|
for name in optional] |
|
628
|
|
|
except resource.ResourceNotFoundError: |
|
629
|
|
|
print(('Action "%s" is not found. ' % args.ref_or_id) + |
|
630
|
|
|
'Do "st2 action list" to see list of available actions.') |
|
631
|
|
|
except Exception as e: |
|
632
|
|
|
print('ERROR: Unable to print help for action "%s". %s' % |
|
633
|
|
|
(args.ref_or_id, e)) |
|
634
|
|
|
else: |
|
635
|
|
|
self.parser.print_help() |
|
636
|
|
|
return True |
|
637
|
|
|
return False |
|
638
|
|
|
|
|
639
|
|
|
@staticmethod |
|
640
|
|
|
def _print_param(name, schema): |
|
641
|
|
|
if not schema: |
|
642
|
|
|
raise ValueError('Missing schema for parameter "%s"' % (name)) |
|
643
|
|
|
|
|
644
|
|
|
wrapper = textwrap.TextWrapper(width=78) |
|
645
|
|
|
wrapper.initial_indent = ' ' * 4 |
|
646
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
|
647
|
|
|
print(wrapper.fill(name)) |
|
648
|
|
|
wrapper.initial_indent = ' ' * 8 |
|
649
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
|
650
|
|
|
if 'description' in schema and schema['description']: |
|
651
|
|
|
print(wrapper.fill(schema['description'])) |
|
652
|
|
|
if 'type' in schema and schema['type']: |
|
653
|
|
|
print(wrapper.fill('Type: %s' % schema['type'])) |
|
654
|
|
|
if 'enum' in schema and schema['enum']: |
|
655
|
|
|
print(wrapper.fill('Enum: %s' % ', '.join(schema['enum']))) |
|
656
|
|
|
if 'default' in schema and schema['default'] is not None: |
|
657
|
|
|
print(wrapper.fill('Default: %s' % schema['default'])) |
|
658
|
|
|
print('') |
|
659
|
|
|
|
|
660
|
|
|
@staticmethod |
|
661
|
|
|
def _get_params_types(runner, action): |
|
662
|
|
|
runner_params = runner.runner_parameters |
|
663
|
|
|
action_params = action.parameters |
|
664
|
|
|
parameters = copy.copy(runner_params) |
|
665
|
|
|
parameters.update(copy.copy(action_params)) |
|
666
|
|
|
required = set([k for k, v in six.iteritems(parameters) if v.get('required')]) |
|
667
|
|
|
|
|
668
|
|
|
def is_immutable(runner_param_meta, action_param_meta): |
|
669
|
|
|
# If runner sets a param as immutable, action cannot override that. |
|
670
|
|
|
if runner_param_meta.get('immutable', False): |
|
671
|
|
|
return True |
|
672
|
|
|
else: |
|
673
|
|
|
return action_param_meta.get('immutable', False) |
|
674
|
|
|
|
|
675
|
|
|
immutable = set() |
|
676
|
|
|
for param in parameters.keys(): |
|
677
|
|
|
if is_immutable(runner_params.get(param, {}), |
|
678
|
|
|
action_params.get(param, {})): |
|
679
|
|
|
immutable.add(param) |
|
680
|
|
|
|
|
681
|
|
|
required = required - immutable |
|
682
|
|
|
optional = set(parameters.keys()) - required - immutable |
|
683
|
|
|
|
|
684
|
|
|
return parameters, required, optional, immutable |
|
685
|
|
|
|
|
686
|
|
|
def _format_child_instances(self, children, parent_id): |
|
687
|
|
|
''' |
|
688
|
|
|
The goal of this method is to add an indent at every level. This way the |
|
689
|
|
|
WF is represented as a tree structure while in a list. For the right visuals |
|
690
|
|
|
representation the list must be a DF traversal else the idents will end up |
|
691
|
|
|
looking strange. |
|
692
|
|
|
''' |
|
693
|
|
|
# apply basic WF formating first. |
|
694
|
|
|
children = format_wf_instances(children) |
|
695
|
|
|
# setup a depth lookup table |
|
696
|
|
|
depth = {parent_id: 0} |
|
697
|
|
|
result = [] |
|
698
|
|
|
# main loop that indents each entry correctly |
|
699
|
|
|
for child in children: |
|
700
|
|
|
# make sure child.parent is in depth and while at it compute the |
|
701
|
|
|
# right depth for indentation purposes. |
|
702
|
|
|
if child.parent not in depth: |
|
703
|
|
|
parent = None |
|
704
|
|
|
for instance in children: |
|
705
|
|
|
if WF_PREFIX in instance.id: |
|
706
|
|
|
instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):] |
|
707
|
|
|
else: |
|
708
|
|
|
instance_id = instance.id |
|
709
|
|
|
if instance_id == child.parent: |
|
710
|
|
|
parent = instance |
|
711
|
|
|
if parent and parent.parent and parent.parent in depth: |
|
712
|
|
|
depth[child.parent] = depth[parent.parent] + 1 |
|
713
|
|
|
else: |
|
714
|
|
|
depth[child.parent] = 0 |
|
715
|
|
|
# now ident for the right visuals |
|
716
|
|
|
child.id = INDENT_CHAR * depth[child.parent] + child.id |
|
717
|
|
|
result.append(self._format_for_common_representation(child)) |
|
718
|
|
|
return result |
|
719
|
|
|
|
|
720
|
|
|
def _format_for_common_representation(self, task): |
|
721
|
|
|
''' |
|
722
|
|
|
Formats a task for common representation between mistral and action-chain. |
|
723
|
|
|
''' |
|
724
|
|
|
# This really needs to be better handled on the back-end but that would be a bigger |
|
725
|
|
|
# change so handling in cli. |
|
726
|
|
|
context = getattr(task, 'context', None) |
|
727
|
|
|
if context and 'chain' in context: |
|
728
|
|
|
task_name_key = 'context.chain.name' |
|
729
|
|
|
elif context and 'mistral' in context: |
|
730
|
|
|
task_name_key = 'context.mistral.task_name' |
|
731
|
|
|
# Use LiveAction as the object so that the formatter lookup does not change. |
|
732
|
|
|
# AKA HACK! |
|
733
|
|
|
return models.action.LiveAction(**{ |
|
734
|
|
|
'id': task.id, |
|
735
|
|
|
'status': task.status, |
|
736
|
|
|
'task': jsutil.get_value(vars(task), task_name_key), |
|
737
|
|
|
'action': task.action.get('ref', None), |
|
738
|
|
|
'start_timestamp': task.start_timestamp, |
|
739
|
|
|
'end_timestamp': getattr(task, 'end_timestamp', None) |
|
740
|
|
|
}) |
|
741
|
|
|
|
|
742
|
|
|
def _sort_parameters(self, parameters, names): |
|
743
|
|
|
""" |
|
744
|
|
|
Sort a provided list of action parameters. |
|
745
|
|
|
|
|
746
|
|
|
:type parameters: ``list`` |
|
747
|
|
|
:type names: ``list`` or ``set`` |
|
748
|
|
|
""" |
|
749
|
|
|
sorted_parameters = sorted(names, key=lambda name: |
|
750
|
|
|
self._get_parameter_sort_value( |
|
751
|
|
|
parameters=parameters, |
|
752
|
|
|
name=name)) |
|
753
|
|
|
|
|
754
|
|
|
return sorted_parameters |
|
755
|
|
|
|
|
756
|
|
|
def _get_parameter_sort_value(self, parameters, name): |
|
757
|
|
|
""" |
|
758
|
|
|
Return a value which determines sort order for a particular parameter. |
|
759
|
|
|
|
|
760
|
|
|
By default, parameters are sorted using "position" parameter attribute. |
|
761
|
|
|
If this attribute is not available, parameter is sorted based on the |
|
762
|
|
|
name. |
|
763
|
|
|
""" |
|
764
|
|
|
parameter = parameters.get(name, None) |
|
765
|
|
|
|
|
766
|
|
|
if not parameter: |
|
767
|
|
|
return None |
|
768
|
|
|
|
|
769
|
|
|
sort_value = parameter.get('position', name) |
|
770
|
|
|
return sort_value |
|
771
|
|
|
|
|
772
|
|
|
def _get_inherited_env_vars(self): |
|
773
|
|
|
env_vars = os.environ.copy() |
|
774
|
|
|
|
|
775
|
|
|
for var_name in ENV_VARS_BLACKLIST: |
|
776
|
|
|
if var_name.lower() in env_vars: |
|
777
|
|
|
del env_vars[var_name.lower()] |
|
778
|
|
|
if var_name.upper() in env_vars: |
|
779
|
|
|
del env_vars[var_name.upper()] |
|
780
|
|
|
|
|
781
|
|
|
return env_vars |
|
782
|
|
|
|
|
783
|
|
|
|
|
784
|
|
|
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
|
785
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
|
|
786
|
|
|
|
|
787
|
|
|
super(ActionRunCommand, self).__init__( |
|
788
|
|
|
resource, kwargs.pop('name', 'execute'), |
|
789
|
|
|
'A command to invoke an action manually.', |
|
790
|
|
|
*args, **kwargs) |
|
791
|
|
|
|
|
792
|
|
|
self.parser.add_argument('ref_or_id', nargs='?', |
|
793
|
|
|
metavar='ref-or-id', |
|
794
|
|
|
help='Action reference (pack.action_name) ' + |
|
795
|
|
|
'or ID of the action.') |
|
796
|
|
|
self.parser.add_argument('parameters', nargs='*', |
|
797
|
|
|
help='List of keyword args, positional args, ' |
|
798
|
|
|
'and optional args for the action.') |
|
799
|
|
|
|
|
800
|
|
|
self.parser.add_argument('-h', '--help', |
|
801
|
|
|
action='store_true', dest='help', |
|
802
|
|
|
help='Print usage for the given action.') |
|
803
|
|
|
|
|
804
|
|
|
self._add_common_options() |
|
805
|
|
|
|
|
806
|
|
|
if self.name in ['run', 'execute']: |
|
807
|
|
|
self.parser.add_argument('--trace-tag', '--trace_tag', |
|
808
|
|
|
help='A trace tag string to track execution later.', |
|
809
|
|
|
dest='trace_tag', required=False) |
|
810
|
|
|
self.parser.add_argument('--trace-id', |
|
811
|
|
|
help='Existing trace id for this execution.', |
|
812
|
|
|
dest='trace_id', required=False) |
|
813
|
|
|
self.parser.add_argument('-a', '--async', |
|
814
|
|
|
action='store_true', dest='async', |
|
815
|
|
|
help='Do not wait for action to finish.') |
|
816
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
|
817
|
|
|
action='store_true', dest='inherit_env', |
|
818
|
|
|
help='Pass all the environment variables ' |
|
819
|
|
|
'which are accessible to the CLI as "env" ' |
|
820
|
|
|
'parameter to the action. Note: Only works ' |
|
821
|
|
|
'with python, local and remote runners.') |
|
822
|
|
|
|
|
823
|
|
|
if self.name == 'run': |
|
824
|
|
|
self.parser.set_defaults(async=False) |
|
825
|
|
|
else: |
|
826
|
|
|
self.parser.set_defaults(async=True) |
|
827
|
|
|
|
|
828
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
829
|
|
|
def run(self, args, **kwargs): |
|
830
|
|
|
if not args.ref_or_id: |
|
831
|
|
|
self.parser.error('Missing action reference or id') |
|
832
|
|
|
|
|
833
|
|
|
action = self.get_resource(args.ref_or_id, **kwargs) |
|
834
|
|
|
if not action: |
|
835
|
|
|
raise resource.ResourceNotFoundError('Action "%s" cannot be found.' |
|
836
|
|
|
% (args.ref_or_id)) |
|
837
|
|
|
|
|
838
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
|
839
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
|
840
|
|
|
if not runner: |
|
841
|
|
|
raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.' |
|
842
|
|
|
% (action.runner_type, action.name)) |
|
843
|
|
|
|
|
844
|
|
|
action_ref = '.'.join([action.pack, action.name]) |
|
845
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
|
846
|
|
|
args=args) |
|
847
|
|
|
|
|
848
|
|
|
execution = models.LiveAction() |
|
849
|
|
|
execution.action = action_ref |
|
850
|
|
|
execution.parameters = action_parameters |
|
851
|
|
|
|
|
852
|
|
|
if not args.trace_id and args.trace_tag: |
|
853
|
|
|
execution.context = {'trace_context': {'trace_tag': args.trace_tag}} |
|
854
|
|
|
|
|
855
|
|
|
if args.trace_id: |
|
856
|
|
|
execution.context = {'trace_context': {'id_': args.trace_id}} |
|
857
|
|
|
|
|
858
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
|
859
|
|
|
|
|
860
|
|
|
execution = action_exec_mgr.create(execution, **kwargs) |
|
861
|
|
|
execution = self._get_execution_result(execution=execution, |
|
862
|
|
|
action_exec_mgr=action_exec_mgr, |
|
863
|
|
|
args=args, **kwargs) |
|
864
|
|
|
return execution |
|
865
|
|
|
|
|
866
|
|
|
|
|
867
|
|
|
class ActionExecutionBranch(resource.ResourceBranch): |
|
868
|
|
|
|
|
869
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
|
870
|
|
|
super(ActionExecutionBranch, self).__init__( |
|
871
|
|
|
models.LiveAction, description, app, subparsers, |
|
872
|
|
|
parent_parser=parent_parser, read_only=True, |
|
873
|
|
|
commands={'list': ActionExecutionListCommand, |
|
874
|
|
|
'get': ActionExecutionGetCommand}) |
|
875
|
|
|
|
|
876
|
|
|
# Register extended commands |
|
877
|
|
|
self.commands['re-run'] = ActionExecutionReRunCommand(self.resource, self.app, |
|
878
|
|
|
self.subparsers, add_help=False) |
|
879
|
|
|
self.commands['cancel'] = ActionExecutionCancelCommand(self.resource, self.app, |
|
880
|
|
|
self.subparsers, add_help=False) |
|
881
|
|
|
|
|
882
|
|
|
|
|
883
|
|
|
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled') |
|
884
|
|
|
|
|
885
|
|
|
|
|
886
|
|
|
class ActionExecutionReadCommand(resource.ResourceCommand): |
|
887
|
|
|
""" |
|
888
|
|
|
Base class for read / view commands (list and get). |
|
889
|
|
|
""" |
|
890
|
|
|
|
|
891
|
|
|
def _get_exclude_attributes(self, args): |
|
892
|
|
|
""" |
|
893
|
|
|
Retrieve a list of exclude attributes for particular command line arguments. |
|
894
|
|
|
""" |
|
895
|
|
|
exclude_attributes = [] |
|
896
|
|
|
|
|
897
|
|
|
if 'result' not in args.attr: |
|
898
|
|
|
exclude_attributes.append('result') |
|
899
|
|
|
if 'trigger_instance' not in args.attr: |
|
900
|
|
|
exclude_attributes.append('trigger_instance') |
|
901
|
|
|
|
|
902
|
|
|
return exclude_attributes |
|
903
|
|
|
|
|
904
|
|
|
|
|
905
|
|
|
class ActionExecutionListCommand(ActionExecutionReadCommand): |
|
906
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp', |
|
907
|
|
|
'end_timestamp'] |
|
908
|
|
|
attribute_transform_functions = { |
|
909
|
|
|
'start_timestamp': format_isodate, |
|
910
|
|
|
'end_timestamp': format_isodate, |
|
911
|
|
|
'parameters': format_parameters, |
|
912
|
|
|
'status': format_status |
|
913
|
|
|
} |
|
914
|
|
|
|
|
915
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
|
|
916
|
|
|
super(ActionExecutionListCommand, self).__init__( |
|
917
|
|
|
resource, 'list', 'Get the list of the 50 most recent %s.' % |
|
918
|
|
|
resource.get_plural_display_name().lower(), |
|
919
|
|
|
*args, **kwargs) |
|
920
|
|
|
|
|
921
|
|
|
self.group = self.parser.add_argument_group() |
|
922
|
|
|
self.parser.add_argument('-n', '--last', type=int, dest='last', |
|
923
|
|
|
default=50, |
|
924
|
|
|
help=('List N most recent %s; ' |
|
925
|
|
|
'list all if 0.' % |
|
926
|
|
|
resource.get_plural_display_name().lower())) |
|
927
|
|
|
|
|
928
|
|
|
# Filter options |
|
929
|
|
|
self.group.add_argument('--action', help='Action reference to filter the list.') |
|
930
|
|
|
self.group.add_argument('--status', help=('Only return executions with the provided status.' |
|
931
|
|
|
' Possible values are \'%s\', \'%s\', \'%s\',' |
|
932
|
|
|
'\'%s\' or \'%s\'' |
|
933
|
|
|
'.' % POSSIBLE_ACTION_STATUS_VALUES)) |
|
934
|
|
|
self.group.add_argument('--trigger_instance', |
|
935
|
|
|
help='Trigger instance id to filter the list.') |
|
936
|
|
|
self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt', |
|
937
|
|
|
default=None, |
|
938
|
|
|
help=('Only return executions with timestamp ' |
|
939
|
|
|
'greater than the one provided. ' |
|
940
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
|
941
|
|
|
self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt', |
|
942
|
|
|
default=None, |
|
943
|
|
|
help=('Only return executions with timestamp ' |
|
944
|
|
|
'lower than the one provided. ' |
|
945
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
|
946
|
|
|
self.parser.add_argument('-l', '--showall', action='store_true', |
|
947
|
|
|
help='') |
|
948
|
|
|
|
|
949
|
|
|
# Display options |
|
950
|
|
|
self.parser.add_argument('-a', '--attr', nargs='+', |
|
951
|
|
|
default=self.display_attributes, |
|
952
|
|
|
help=('List of attributes to include in the ' |
|
953
|
|
|
'output. "all" will return all ' |
|
954
|
|
|
'attributes.')) |
|
955
|
|
|
self.parser.add_argument('-w', '--width', nargs='+', type=int, |
|
956
|
|
|
default=None, |
|
957
|
|
|
help=('Set the width of columns in output.')) |
|
958
|
|
|
|
|
959
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
960
|
|
|
def run(self, args, **kwargs): |
|
961
|
|
|
# Filtering options |
|
962
|
|
|
if args.action: |
|
963
|
|
|
kwargs['action'] = args.action |
|
964
|
|
|
if args.status: |
|
965
|
|
|
kwargs['status'] = args.status |
|
966
|
|
|
if args.trigger_instance: |
|
967
|
|
|
kwargs['trigger_instance'] = args.trigger_instance |
|
968
|
|
|
if not args.showall: |
|
969
|
|
|
# null is the magic string that translates to does not exist. |
|
970
|
|
|
kwargs['parent'] = 'null' |
|
971
|
|
|
if args.timestamp_gt: |
|
972
|
|
|
kwargs['timestamp_gt'] = args.timestamp_gt |
|
973
|
|
|
if args.timestamp_lt: |
|
974
|
|
|
kwargs['timestamp_lt'] = args.timestamp_lt |
|
975
|
|
|
|
|
976
|
|
|
# We exclude "result" and "trigger_instance" attributes which can contain a lot of data |
|
977
|
|
|
# since they are not displayed nor used which speeds the common operation substantially. |
|
978
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
|
979
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
|
980
|
|
|
kwargs['exclude_attributes'] = exclude_attributes |
|
981
|
|
|
|
|
982
|
|
|
return self.manager.query(limit=args.last, **kwargs) |
|
983
|
|
|
|
|
984
|
|
|
def run_and_print(self, args, **kwargs): |
|
985
|
|
|
instances = format_wf_instances(self.run(args, **kwargs)) |
|
986
|
|
|
|
|
987
|
|
|
if not args.json: |
|
988
|
|
|
# Include elapsed time for running executions |
|
989
|
|
|
instances = format_execution_statuses(instances) |
|
990
|
|
|
|
|
991
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
|
992
|
|
|
attributes=args.attr, widths=args.width, |
|
993
|
|
|
json=args.json, |
|
994
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
|
995
|
|
|
|
|
996
|
|
|
|
|
997
|
|
|
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
|
998
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
|
999
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
|
1000
|
|
|
|
|
1001
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
|
|
1002
|
|
|
super(ActionExecutionGetCommand, self).__init__( |
|
1003
|
|
|
resource, 'get', |
|
1004
|
|
|
'Get individual %s.' % resource.get_display_name().lower(), |
|
1005
|
|
|
*args, **kwargs) |
|
1006
|
|
|
|
|
1007
|
|
|
self.parser.add_argument('id', |
|
1008
|
|
|
help=('ID of the %s.' % |
|
1009
|
|
|
resource.get_display_name().lower())) |
|
1010
|
|
|
|
|
1011
|
|
|
self._add_common_options() |
|
1012
|
|
|
|
|
1013
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
1014
|
|
|
def run(self, args, **kwargs): |
|
1015
|
|
|
# We exclude "result" and / or "trigger_instance" attribute if it's not explicitly |
|
1016
|
|
|
# requested by user either via "--attr" flag or by default. |
|
1017
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
|
1018
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
|
1019
|
|
|
|
|
1020
|
|
|
kwargs['params'] = {'exclude_attributes': exclude_attributes} |
|
1021
|
|
|
|
|
1022
|
|
|
execution = self.get_resource_by_id(id=args.id, **kwargs) |
|
1023
|
|
|
return execution |
|
1024
|
|
|
|
|
1025
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
1026
|
|
|
def run_and_print(self, args, **kwargs): |
|
1027
|
|
|
try: |
|
1028
|
|
|
execution = self.run(args, **kwargs) |
|
1029
|
|
|
|
|
1030
|
|
|
if not args.json: |
|
1031
|
|
|
# Include elapsed time for running executions |
|
1032
|
|
|
execution = format_execution_status(execution) |
|
1033
|
|
|
except resource.ResourceNotFoundError: |
|
1034
|
|
|
self.print_not_found(args.id) |
|
1035
|
|
|
raise OperationFailureException('Execution %s not found.' % (args.id)) |
|
1036
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
|
1037
|
|
|
|
|
1038
|
|
|
|
|
1039
|
|
|
class ActionExecutionCancelCommand(resource.ResourceCommand): |
|
1040
|
|
|
|
|
1041
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
|
|
1042
|
|
|
super(ActionExecutionCancelCommand, self).__init__( |
|
1043
|
|
|
resource, 'cancel', 'Cancel an %s.' % |
|
1044
|
|
|
resource.get_plural_display_name().lower(), |
|
1045
|
|
|
*args, **kwargs) |
|
1046
|
|
|
|
|
1047
|
|
|
self.parser.add_argument('id', |
|
1048
|
|
|
help=('ID of the %s.' % |
|
1049
|
|
|
resource.get_display_name().lower())) |
|
1050
|
|
|
|
|
1051
|
|
|
def run(self, args, **kwargs): |
|
1052
|
|
|
return self.manager.delete_by_id(args.id) |
|
1053
|
|
|
|
|
1054
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
1055
|
|
|
def run_and_print(self, args, **kwargs): |
|
1056
|
|
|
response = self.run(args, **kwargs) |
|
1057
|
|
|
if response and 'faultstring' in response: |
|
1058
|
|
|
message = response.get('faultstring', 'Cancellation requested for %s with id %s.' % |
|
1059
|
|
|
(self.resource.get_display_name().lower(), args.id)) |
|
1060
|
|
|
|
|
1061
|
|
|
elif response: |
|
1062
|
|
|
message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(), |
|
1063
|
|
|
args.id) |
|
1064
|
|
|
else: |
|
1065
|
|
|
message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(), |
|
1066
|
|
|
args.id) |
|
1067
|
|
|
print(message) |
|
1068
|
|
|
|
|
1069
|
|
|
|
|
1070
|
|
|
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
|
1071
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
|
|
1072
|
|
|
|
|
1073
|
|
|
super(ActionExecutionReRunCommand, self).__init__( |
|
1074
|
|
|
resource, kwargs.pop('name', 're-run'), |
|
1075
|
|
|
'A command to re-run a particular action.', |
|
1076
|
|
|
*args, **kwargs) |
|
1077
|
|
|
|
|
1078
|
|
|
self.parser.add_argument('id', nargs='?', |
|
1079
|
|
|
metavar='id', |
|
1080
|
|
|
help='ID of action execution to re-run ') |
|
1081
|
|
|
self.parser.add_argument('parameters', nargs='*', |
|
1082
|
|
|
help='List of keyword args, positional args, ' |
|
1083
|
|
|
'and optional args for the action.') |
|
1084
|
|
|
self.parser.add_argument('--tasks', nargs='*', |
|
1085
|
|
|
help='Name of the workflow tasks to re-run.') |
|
1086
|
|
|
self.parser.add_argument('--no-reset', dest='no_reset', nargs='*', |
|
1087
|
|
|
help='Name of the with-items tasks to not reset. This only ' |
|
1088
|
|
|
'applies to Mistral workflows. By default, all iterations ' |
|
1089
|
|
|
'for with-items tasks is rerun. If no reset, only failed ' |
|
1090
|
|
|
' iterations are rerun.') |
|
1091
|
|
|
self.parser.add_argument('-a', '--async', |
|
1092
|
|
|
action='store_true', dest='async', |
|
1093
|
|
|
help='Do not wait for action to finish.') |
|
1094
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
|
1095
|
|
|
action='store_true', dest='inherit_env', |
|
1096
|
|
|
help='Pass all the environment variables ' |
|
1097
|
|
|
'which are accessible to the CLI as "env" ' |
|
1098
|
|
|
'parameter to the action. Note: Only works ' |
|
1099
|
|
|
'with python, local and remote runners.') |
|
1100
|
|
|
self.parser.add_argument('-h', '--help', |
|
1101
|
|
|
action='store_true', dest='help', |
|
1102
|
|
|
help='Print usage for the given action.') |
|
1103
|
|
|
|
|
1104
|
|
|
self._add_common_options() |
|
1105
|
|
|
|
|
1106
|
|
|
@add_auth_token_to_kwargs_from_cli |
|
1107
|
|
|
def run(self, args, **kwargs): |
|
1108
|
|
|
existing_execution = self.manager.get_by_id(args.id, **kwargs) |
|
1109
|
|
|
|
|
1110
|
|
|
if not existing_execution: |
|
1111
|
|
|
raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' % |
|
1112
|
|
|
(args.id)) |
|
1113
|
|
|
|
|
1114
|
|
|
action_mgr = self.app.client.managers['Action'] |
|
1115
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
|
1116
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
|
1117
|
|
|
|
|
1118
|
|
|
action_ref = existing_execution.action['ref'] |
|
1119
|
|
|
action = action_mgr.get_by_ref_or_id(action_ref) |
|
1120
|
|
|
runner = runner_mgr.get_by_name(action.runner_type) |
|
1121
|
|
|
|
|
1122
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
|
1123
|
|
|
args=args) |
|
1124
|
|
|
|
|
1125
|
|
|
execution = action_exec_mgr.re_run(execution_id=args.id, |
|
1126
|
|
|
parameters=action_parameters, |
|
1127
|
|
|
tasks=args.tasks, |
|
1128
|
|
|
no_reset=args.no_reset, |
|
1129
|
|
|
**kwargs) |
|
1130
|
|
|
|
|
1131
|
|
|
execution = self._get_execution_result(execution=execution, |
|
1132
|
|
|
action_exec_mgr=action_exec_mgr, |
|
1133
|
|
|
args=args, **kwargs) |
|
1134
|
|
|
|
|
1135
|
|
|
return execution |
|
1136
|
|
|
|
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: