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_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
|
|
|
else: |
262
|
|
|
self._print_execution_details(execution=execution, args=args, **kwargs) |
263
|
|
|
|
264
|
|
|
if execution.status == 'failed': |
265
|
|
|
# Exit with non zero if the action has failed |
266
|
|
|
sys.exit(1) |
267
|
|
|
|
268
|
|
|
def _add_common_options(self): |
269
|
|
|
root_arg_grp = self.parser.add_mutually_exclusive_group() |
270
|
|
|
|
271
|
|
|
# Display options |
272
|
|
|
task_list_arg_grp = root_arg_grp.add_argument_group() |
273
|
|
|
task_list_arg_grp.add_argument('--raw', action='store_true', |
274
|
|
|
help='Raw output, don\'t shot sub-tasks for workflows.') |
275
|
|
|
task_list_arg_grp.add_argument('--show-tasks', action='store_true', |
276
|
|
|
help='Whether to show sub-tasks of an execution.') |
277
|
|
|
task_list_arg_grp.add_argument('--depth', type=int, default=-1, |
278
|
|
|
help='Depth to which to show sub-tasks. \ |
279
|
|
|
By default all are shown.') |
280
|
|
|
task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None, |
281
|
|
|
help='Set the width of columns in output.') |
282
|
|
|
|
283
|
|
|
execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group() |
284
|
|
|
|
285
|
|
|
detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
286
|
|
|
detail_arg_grp.add_argument('--attr', nargs='+', |
287
|
|
|
default=['id', 'status', 'parameters', 'result'], |
288
|
|
|
help=('List of attributes to include in the ' |
289
|
|
|
'output. "all" or unspecified will ' |
290
|
|
|
'return all attributes.')) |
291
|
|
|
detail_arg_grp.add_argument('-d', '--detail', action='store_true', |
292
|
|
|
help='Display full detail of the execution in table format.') |
293
|
|
|
|
294
|
|
|
result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group() |
295
|
|
|
result_arg_grp.add_argument('-k', '--key', |
296
|
|
|
help=('If result is type of JSON, then print specific ' |
297
|
|
|
'key-value pair; dot notation for nested JSON is ' |
298
|
|
|
'supported.')) |
299
|
|
|
|
300
|
|
|
return root_arg_grp |
301
|
|
|
|
302
|
|
|
def _print_execution_details(self, execution, args, **kwargs): |
303
|
|
|
""" |
304
|
|
|
Print the execution detail to stdout. |
305
|
|
|
|
306
|
|
|
This method takes into account if an executed action was workflow or not |
307
|
|
|
and formats the output accordingly. |
308
|
|
|
""" |
309
|
|
|
runner_type = execution.action.get('runner_type', 'unknown') |
310
|
|
|
is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES |
311
|
|
|
|
312
|
|
|
show_tasks = getattr(args, 'show_tasks', False) |
313
|
|
|
raw = getattr(args, 'raw', False) |
314
|
|
|
detail = getattr(args, 'detail', False) |
315
|
|
|
key = getattr(args, 'key', None) |
316
|
|
|
attr = getattr(args, 'attr', []) |
317
|
|
|
|
318
|
|
|
if show_tasks and not is_workflow_action: |
319
|
|
|
raise ValueError('--show-tasks option can only be used with workflow actions') |
320
|
|
|
|
321
|
|
|
if not raw and not detail and (show_tasks or is_workflow_action): |
322
|
|
|
self._run_and_print_child_task_list(execution=execution, args=args, **kwargs) |
323
|
|
|
else: |
324
|
|
|
instance = execution |
325
|
|
|
|
326
|
|
|
if detail: |
327
|
|
|
formatter = table.PropertyValueTable |
328
|
|
|
else: |
329
|
|
|
formatter = execution_formatter.ExecutionResult |
330
|
|
|
|
331
|
|
|
if detail: |
332
|
|
|
options = {'attributes': copy.copy(self.display_attributes)} |
333
|
|
|
elif key: |
334
|
|
|
options = {'attributes': ['result.%s' % (key)], 'key': key} |
335
|
|
|
else: |
336
|
|
|
options = {'attributes': attr} |
337
|
|
|
|
338
|
|
|
options['json'] = args.json |
339
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
340
|
|
|
self.print_output(instance, formatter, **options) |
341
|
|
|
|
342
|
|
|
def _run_and_print_child_task_list(self, execution, args, **kwargs): |
343
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
344
|
|
|
|
345
|
|
|
instance = execution |
346
|
|
|
options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp', |
347
|
|
|
'end_timestamp']} |
348
|
|
|
options['json'] = args.json |
349
|
|
|
options['attribute_transform_functions'] = self.attribute_transform_functions |
350
|
|
|
formatter = execution_formatter.ExecutionResult |
351
|
|
|
|
352
|
|
|
kwargs['depth'] = args.depth |
353
|
|
|
child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs) |
354
|
|
|
child_instances = self._format_child_instances(child_instances, execution.id) |
355
|
|
|
child_instances = format_execution_statuses(child_instances) |
356
|
|
|
|
357
|
|
|
if not child_instances: |
358
|
|
|
# No child error, there might be a global error, include result in the output |
359
|
|
|
options['attributes'].append('result') |
360
|
|
|
|
361
|
|
|
status_index = options['attributes'].index('status') |
362
|
|
|
|
363
|
|
|
if hasattr(instance, 'result') and isinstance(instance.result, dict): |
364
|
|
|
tasks = instance.result.get('tasks', []) |
365
|
|
|
else: |
366
|
|
|
tasks = [] |
367
|
|
|
|
368
|
|
|
# On failure we also want to include error message and traceback at the top level |
369
|
|
|
if instance.status == 'failed': |
370
|
|
|
top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance) |
371
|
|
|
|
372
|
|
|
if len(tasks) >= 1: |
373
|
|
|
task_error, task_traceback = self._get_task_error(task=tasks[-1]) |
374
|
|
|
else: |
375
|
|
|
task_error, task_traceback = None, None |
376
|
|
|
|
377
|
|
|
if top_level_error: |
378
|
|
|
# Top-level error |
379
|
|
|
instance.error = top_level_error |
380
|
|
|
instance.traceback = top_level_traceback |
381
|
|
|
instance.result = 'See error and traceback.' |
382
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
383
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
384
|
|
|
elif task_error: |
385
|
|
|
# Task error |
386
|
|
|
instance.error = task_error |
387
|
|
|
instance.traceback = task_traceback |
388
|
|
|
instance.result = 'See error and traceback.' |
389
|
|
|
instance.failed_on = tasks[-1].get('name', 'unknown') |
390
|
|
|
options['attributes'].insert(status_index + 1, 'error') |
391
|
|
|
options['attributes'].insert(status_index + 2, 'traceback') |
392
|
|
|
options['attributes'].insert(status_index + 3, 'failed_on') |
393
|
|
|
|
394
|
|
|
# Include result on the top-level object so user doesn't need to issue another command to |
395
|
|
|
# see the result |
396
|
|
|
if len(tasks) >= 1: |
397
|
|
|
task_result = self._get_task_result(task=tasks[-1]) |
398
|
|
|
|
399
|
|
|
if task_result: |
400
|
|
|
instance.result_task = tasks[-1].get('name', 'unknown') |
401
|
|
|
options['attributes'].insert(status_index + 1, 'result_task') |
402
|
|
|
options['attributes'].insert(status_index + 2, 'result') |
403
|
|
|
instance.result = task_result |
404
|
|
|
|
405
|
|
|
# print root task |
406
|
|
|
self.print_output(instance, formatter, **options) |
407
|
|
|
|
408
|
|
|
# print child tasks |
409
|
|
|
if child_instances: |
410
|
|
|
self.print_output(child_instances, table.MultiColumnTable, |
411
|
|
|
attributes=['id', 'status', 'task', 'action', 'start_timestamp'], |
412
|
|
|
widths=args.width, json=args.json, |
413
|
|
|
yaml=args.yaml, |
414
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
415
|
|
|
|
416
|
|
|
def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): |
417
|
|
|
pending_statuses = [ |
418
|
|
|
LIVEACTION_STATUS_REQUESTED, |
419
|
|
|
LIVEACTION_STATUS_SCHEDULED, |
420
|
|
|
LIVEACTION_STATUS_RUNNING, |
421
|
|
|
LIVEACTION_STATUS_CANCELING |
422
|
|
|
] |
423
|
|
|
|
424
|
|
|
if not args.async: |
425
|
|
|
while execution.status in pending_statuses: |
426
|
|
|
time.sleep(self.poll_interval) |
427
|
|
|
if not args.json and not args.yaml: |
428
|
|
|
sys.stdout.write('.') |
429
|
|
|
sys.stdout.flush() |
430
|
|
|
execution = action_exec_mgr.get_by_id(execution.id, **kwargs) |
431
|
|
|
|
432
|
|
|
sys.stdout.write('\n') |
433
|
|
|
|
434
|
|
|
if execution.status == LIVEACTION_STATUS_CANCELED: |
435
|
|
|
return execution |
436
|
|
|
|
437
|
|
|
return execution |
438
|
|
|
|
439
|
|
|
def _get_top_level_error(self, live_action): |
440
|
|
|
""" |
441
|
|
|
Retrieve a top level workflow error. |
442
|
|
|
|
443
|
|
|
:return: (error, traceback) |
444
|
|
|
""" |
445
|
|
|
if isinstance(live_action.result, dict): |
446
|
|
|
error = live_action.result.get('error', None) |
447
|
|
|
traceback = live_action.result.get('traceback', None) |
448
|
|
|
else: |
449
|
|
|
error = "See result" |
450
|
|
|
traceback = "See result" |
451
|
|
|
|
452
|
|
|
return error, traceback |
453
|
|
|
|
454
|
|
|
def _get_task_error(self, task): |
455
|
|
|
""" |
456
|
|
|
Retrieve error message from the provided task. |
457
|
|
|
|
458
|
|
|
:return: (error, traceback) |
459
|
|
|
""" |
460
|
|
|
if not task: |
461
|
|
|
return None, None |
462
|
|
|
|
463
|
|
|
result = task['result'] |
464
|
|
|
|
465
|
|
|
if isinstance(result, dict): |
466
|
|
|
stderr = result.get('stderr', None) |
467
|
|
|
error = result.get('error', None) |
468
|
|
|
traceback = result.get('traceback', None) |
469
|
|
|
error = error if error else stderr |
470
|
|
|
else: |
471
|
|
|
stderr = None |
472
|
|
|
error = None |
473
|
|
|
traceback = None |
474
|
|
|
|
475
|
|
|
return error, traceback |
476
|
|
|
|
477
|
|
|
def _get_task_result(self, task): |
478
|
|
|
if not task: |
479
|
|
|
return None |
480
|
|
|
|
481
|
|
|
return task['result'] |
482
|
|
|
|
483
|
|
|
def _get_action_parameters_from_args(self, action, runner, args): |
484
|
|
|
""" |
485
|
|
|
Build a dictionary with parameters which will be passed to the action by |
486
|
|
|
parsing parameters passed to the CLI. |
487
|
|
|
|
488
|
|
|
:param args: CLI argument. |
489
|
|
|
:type args: ``object`` |
490
|
|
|
|
491
|
|
|
:rtype: ``dict`` |
492
|
|
|
""" |
493
|
|
|
action_ref_or_id = action.ref |
494
|
|
|
|
495
|
|
|
def read_file(file_path): |
496
|
|
|
if not os.path.exists(file_path): |
497
|
|
|
raise ValueError('File "%s" doesn\'t exist' % (file_path)) |
498
|
|
|
|
499
|
|
|
if not os.path.isfile(file_path): |
500
|
|
|
raise ValueError('"%s" is not a file' % (file_path)) |
501
|
|
|
|
502
|
|
|
with open(file_path, 'rb') as fp: |
503
|
|
|
content = fp.read() |
504
|
|
|
|
505
|
|
|
return content |
506
|
|
|
|
507
|
|
|
def transform_object(value): |
508
|
|
|
# Also support simple key1=val1,key2=val2 syntax |
509
|
|
|
if value.startswith('{'): |
510
|
|
|
# Assume it's JSON |
511
|
|
|
result = value = json.loads(value) |
512
|
|
|
else: |
513
|
|
|
pairs = value.split(',') |
514
|
|
|
|
515
|
|
|
result = {} |
516
|
|
|
for pair in pairs: |
517
|
|
|
split = pair.split('=', 1) |
518
|
|
|
|
519
|
|
|
if len(split) != 2: |
520
|
|
|
continue |
521
|
|
|
|
522
|
|
|
key, value = split |
523
|
|
|
result[key] = value |
524
|
|
|
return result |
525
|
|
|
|
526
|
|
|
def transform_array(value, action_params=None): |
527
|
|
|
action_params = action_params or {} |
528
|
|
|
|
529
|
|
|
# Sometimes an array parameter only has a single element: |
530
|
|
|
# |
531
|
|
|
# i.e. "st2 run foopack.fooaction arrayparam=51" |
532
|
|
|
# |
533
|
|
|
# Normally, json.loads would throw an exception, and the split method |
534
|
|
|
# would be used. However, since this is an int, not only would |
535
|
|
|
# splitting not work, but json.loads actually treats this as valid JSON, |
536
|
|
|
# but as an int, not an array. This causes a mismatch when the API is called. |
537
|
|
|
# |
538
|
|
|
# We want to try to handle this first, so it doesn't get accidentally |
539
|
|
|
# sent to the API as an int, instead of an array of single-element int. |
540
|
|
|
try: |
541
|
|
|
# Force this to be a list containing the single int, then |
542
|
|
|
# cast the whole thing to string so json.loads can handle it |
543
|
|
|
value = str([int(value)]) |
544
|
|
|
except ValueError: |
545
|
|
|
# Original value wasn't an int, so just let it continue |
546
|
|
|
pass |
547
|
|
|
|
548
|
|
|
# At this point, the input is either a a "json.loads"-able construct |
549
|
|
|
# like [1, 2, 3], or even [1], or it is a comma-separated list, |
550
|
|
|
# Try both, in that order. |
551
|
|
|
try: |
552
|
|
|
result = json.loads(value) |
553
|
|
|
except ValueError: |
554
|
|
|
result = [v.strip() for v in value.split(',')] |
555
|
|
|
|
556
|
|
|
# When each values in this array represent dict type, this converts |
557
|
|
|
# the 'result' to the dict type value. |
558
|
|
|
if all([isinstance(x, str) and ':' in x for x in result]): |
559
|
|
|
result_dict = {} |
560
|
|
|
for (k, v) in [x.split(':') for x in result]: |
561
|
|
|
# To parse values using the 'transformer' according to the type which is |
562
|
|
|
# specified in the action metadata, calling 'normalize' method recursively. |
563
|
|
|
if 'properties' in action_params and k in action_params['properties']: |
564
|
|
|
result_dict[k] = normalize(k, v, action_params['properties']) |
565
|
|
|
else: |
566
|
|
|
result_dict[k] = v |
567
|
|
|
return [result_dict] |
568
|
|
|
|
569
|
|
|
return result |
570
|
|
|
|
571
|
|
|
transformer = { |
572
|
|
|
'array': transform_array, |
573
|
|
|
'boolean': (lambda x: ast.literal_eval(x.capitalize())), |
574
|
|
|
'integer': int, |
575
|
|
|
'number': float, |
576
|
|
|
'object': transform_object, |
577
|
|
|
'string': str |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
def get_param_type(key, action_params=None): |
581
|
|
|
action_params = action_params or action.parameters |
582
|
|
|
|
583
|
|
|
param = None |
584
|
|
|
if key in runner.runner_parameters: |
585
|
|
|
param = runner.runner_parameters[key] |
586
|
|
|
elif key in action_params: |
587
|
|
|
param = action_params[key] |
588
|
|
|
|
589
|
|
|
if param: |
590
|
|
|
return param['type'] |
591
|
|
|
|
592
|
|
|
return None |
593
|
|
|
|
594
|
|
|
def normalize(name, value, action_params=None): |
595
|
|
|
""" The desired type is contained in the action meta-data, so we can look that up |
596
|
|
|
and call the desired "caster" function listed in the "transformer" dict |
597
|
|
|
""" |
598
|
|
|
action_params = action_params or action.parameters |
599
|
|
|
|
600
|
|
|
# By default, this method uses a parameter which is defined in the action metadata. |
601
|
|
|
# This method assume to be called recursively for parsing values in an array of objects |
602
|
|
|
# type value according to the nested action metadata definition. |
603
|
|
|
# |
604
|
|
|
# This is a best practice to pass a list value as default argument to prevent |
605
|
|
|
# unforeseen consequence by being created a persistent object. |
606
|
|
|
|
607
|
|
|
# Users can also specify type for each array parameter inside an action metadata |
608
|
|
|
# (items: type: int for example) and this information is available here so we could |
609
|
|
|
# also leverage that to cast each array item to the correct type. |
610
|
|
|
param_type = get_param_type(name, action_params) |
611
|
|
|
if param_type == 'array' and name in action_params: |
612
|
|
|
return transformer[param_type](value, action_params[name]) |
613
|
|
|
elif param_type: |
614
|
|
|
return transformer[param_type](value) |
615
|
|
|
|
616
|
|
|
return value |
617
|
|
|
|
618
|
|
|
result = {} |
619
|
|
|
|
620
|
|
|
if not args.parameters: |
621
|
|
|
return result |
622
|
|
|
|
623
|
|
|
for idx in range(len(args.parameters)): |
624
|
|
|
arg = args.parameters[idx] |
625
|
|
|
if '=' in arg: |
626
|
|
|
k, v = arg.split('=', 1) |
627
|
|
|
|
628
|
|
|
# Attribute for files are prefixed with "@" |
629
|
|
|
if k.startswith('@'): |
630
|
|
|
k = k[1:] |
631
|
|
|
is_file = True |
632
|
|
|
else: |
633
|
|
|
is_file = False |
634
|
|
|
|
635
|
|
|
try: |
636
|
|
|
if is_file: |
637
|
|
|
# Files are handled a bit differently since we ship the content |
638
|
|
|
# over the wire |
639
|
|
|
file_path = os.path.normpath(pjoin(os.getcwd(), v)) |
640
|
|
|
file_name = os.path.basename(file_path) |
641
|
|
|
content = read_file(file_path=file_path) |
642
|
|
|
|
643
|
|
|
if action_ref_or_id == 'core.http': |
644
|
|
|
# Special case for http runner |
645
|
|
|
result['_file_name'] = file_name |
646
|
|
|
result['file_content'] = content |
647
|
|
|
else: |
648
|
|
|
result[k] = content |
649
|
|
|
else: |
650
|
|
|
# This permits multiple declarations of argument only in the array type. |
651
|
|
|
if get_param_type(k) == 'array' and k in result: |
652
|
|
|
result[k] += normalize(k, v) |
653
|
|
|
else: |
654
|
|
|
result[k] = normalize(k, v) |
655
|
|
|
|
656
|
|
|
except Exception as e: |
657
|
|
|
# TODO: Move transformers in a separate module and handle |
658
|
|
|
# exceptions there |
659
|
|
|
if 'malformed string' in str(e): |
660
|
|
|
message = ('Invalid value for boolean parameter. ' |
661
|
|
|
'Valid values are: true, false') |
662
|
|
|
raise ValueError(message) |
663
|
|
|
else: |
664
|
|
|
raise e |
665
|
|
|
else: |
666
|
|
|
result['cmd'] = ' '.join(args.parameters[idx:]) |
667
|
|
|
break |
668
|
|
|
|
669
|
|
|
# Special case for http runner |
670
|
|
|
if 'file_content' in result: |
671
|
|
|
if 'method' not in result: |
672
|
|
|
# Default to POST if a method is not provided |
673
|
|
|
result['method'] = 'POST' |
674
|
|
|
|
675
|
|
|
if 'file_name' not in result: |
676
|
|
|
# File name not provided, use default file name |
677
|
|
|
result['file_name'] = result['_file_name'] |
678
|
|
|
|
679
|
|
|
del result['_file_name'] |
680
|
|
|
|
681
|
|
|
if args.inherit_env: |
682
|
|
|
result['env'] = self._get_inherited_env_vars() |
683
|
|
|
|
684
|
|
|
return result |
685
|
|
|
|
686
|
|
|
@add_auth_token_to_kwargs_from_cli |
687
|
|
|
def _print_help(self, args, **kwargs): |
688
|
|
|
# Print appropriate help message if the help option is given. |
689
|
|
|
action_mgr = self.app.client.managers['Action'] |
690
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
691
|
|
|
|
692
|
|
|
if args.help: |
693
|
|
|
action_ref_or_id = getattr(args, 'ref_or_id', None) |
694
|
|
|
action_exec_id = getattr(args, 'id', None) |
695
|
|
|
|
696
|
|
|
if action_exec_id and not action_ref_or_id: |
697
|
|
|
action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs) |
698
|
|
|
args.ref_or_id = action_exec.action |
699
|
|
|
|
700
|
|
|
if action_ref_or_id: |
701
|
|
|
try: |
702
|
|
|
action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs) |
703
|
|
|
if not action: |
704
|
|
|
raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id) |
705
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
706
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
707
|
|
|
parameters, required, optional, _ = self._get_params_types(runner, |
708
|
|
|
action) |
709
|
|
|
print('') |
710
|
|
|
print(textwrap.fill(action.description)) |
711
|
|
|
print('') |
712
|
|
|
if required: |
713
|
|
|
required = self._sort_parameters(parameters=parameters, |
714
|
|
|
names=required) |
715
|
|
|
|
716
|
|
|
print('Required Parameters:') |
717
|
|
|
[self._print_param(name, parameters.get(name)) |
718
|
|
|
for name in required] |
719
|
|
|
if optional: |
720
|
|
|
optional = self._sort_parameters(parameters=parameters, |
721
|
|
|
names=optional) |
722
|
|
|
|
723
|
|
|
print('Optional Parameters:') |
724
|
|
|
[self._print_param(name, parameters.get(name)) |
725
|
|
|
for name in optional] |
726
|
|
|
except resource.ResourceNotFoundError: |
727
|
|
|
print(('Action "%s" is not found. ' % args.ref_or_id) + |
728
|
|
|
'Do "st2 action list" to see list of available actions.') |
729
|
|
|
except Exception as e: |
730
|
|
|
print('ERROR: Unable to print help for action "%s". %s' % |
731
|
|
|
(args.ref_or_id, e)) |
732
|
|
|
else: |
733
|
|
|
self.parser.print_help() |
734
|
|
|
return True |
735
|
|
|
return False |
736
|
|
|
|
737
|
|
|
@staticmethod |
738
|
|
|
def _print_param(name, schema): |
739
|
|
|
if not schema: |
740
|
|
|
raise ValueError('Missing schema for parameter "%s"' % (name)) |
741
|
|
|
|
742
|
|
|
wrapper = textwrap.TextWrapper(width=78) |
743
|
|
|
wrapper.initial_indent = ' ' * 4 |
744
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
745
|
|
|
print(wrapper.fill(name)) |
746
|
|
|
wrapper.initial_indent = ' ' * 8 |
747
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
748
|
|
|
if 'description' in schema and schema['description']: |
749
|
|
|
print(wrapper.fill(schema['description'])) |
750
|
|
|
if 'type' in schema and schema['type']: |
751
|
|
|
print(wrapper.fill('Type: %s' % schema['type'])) |
752
|
|
|
if 'enum' in schema and schema['enum']: |
753
|
|
|
print(wrapper.fill('Enum: %s' % ', '.join(schema['enum']))) |
754
|
|
|
if 'default' in schema and schema['default'] is not None: |
755
|
|
|
print(wrapper.fill('Default: %s' % schema['default'])) |
756
|
|
|
print('') |
757
|
|
|
|
758
|
|
|
@staticmethod |
759
|
|
|
def _get_params_types(runner, action): |
760
|
|
|
runner_params = runner.runner_parameters |
761
|
|
|
action_params = action.parameters |
762
|
|
|
parameters = copy.copy(runner_params) |
763
|
|
|
parameters.update(copy.copy(action_params)) |
764
|
|
|
required = set([k for k, v in six.iteritems(parameters) if v.get('required')]) |
765
|
|
|
|
766
|
|
|
def is_immutable(runner_param_meta, action_param_meta): |
767
|
|
|
# If runner sets a param as immutable, action cannot override that. |
768
|
|
|
if runner_param_meta.get('immutable', False): |
769
|
|
|
return True |
770
|
|
|
else: |
771
|
|
|
return action_param_meta.get('immutable', False) |
772
|
|
|
|
773
|
|
|
immutable = set() |
774
|
|
|
for param in parameters.keys(): |
775
|
|
|
if is_immutable(runner_params.get(param, {}), |
776
|
|
|
action_params.get(param, {})): |
777
|
|
|
immutable.add(param) |
778
|
|
|
|
779
|
|
|
required = required - immutable |
780
|
|
|
optional = set(parameters.keys()) - required - immutable |
781
|
|
|
|
782
|
|
|
return parameters, required, optional, immutable |
783
|
|
|
|
784
|
|
|
def _format_child_instances(self, children, parent_id): |
785
|
|
|
''' |
786
|
|
|
The goal of this method is to add an indent at every level. This way the |
787
|
|
|
WF is represented as a tree structure while in a list. For the right visuals |
788
|
|
|
representation the list must be a DF traversal else the idents will end up |
789
|
|
|
looking strange. |
790
|
|
|
''' |
791
|
|
|
# apply basic WF formating first. |
792
|
|
|
children = format_wf_instances(children) |
793
|
|
|
# setup a depth lookup table |
794
|
|
|
depth = {parent_id: 0} |
795
|
|
|
result = [] |
796
|
|
|
# main loop that indents each entry correctly |
797
|
|
|
for child in children: |
798
|
|
|
# make sure child.parent is in depth and while at it compute the |
799
|
|
|
# right depth for indentation purposes. |
800
|
|
|
if child.parent not in depth: |
801
|
|
|
parent = None |
802
|
|
|
for instance in children: |
803
|
|
|
if WF_PREFIX in instance.id: |
804
|
|
|
instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):] |
805
|
|
|
else: |
806
|
|
|
instance_id = instance.id |
807
|
|
|
if instance_id == child.parent: |
808
|
|
|
parent = instance |
809
|
|
|
if parent and parent.parent and parent.parent in depth: |
810
|
|
|
depth[child.parent] = depth[parent.parent] + 1 |
811
|
|
|
else: |
812
|
|
|
depth[child.parent] = 0 |
813
|
|
|
# now ident for the right visuals |
814
|
|
|
child.id = INDENT_CHAR * depth[child.parent] + child.id |
815
|
|
|
result.append(self._format_for_common_representation(child)) |
816
|
|
|
return result |
817
|
|
|
|
818
|
|
|
def _format_for_common_representation(self, task): |
819
|
|
|
''' |
820
|
|
|
Formats a task for common representation between mistral and action-chain. |
821
|
|
|
''' |
822
|
|
|
# This really needs to be better handled on the back-end but that would be a bigger |
823
|
|
|
# change so handling in cli. |
824
|
|
|
context = getattr(task, 'context', None) |
825
|
|
|
if context and 'chain' in context: |
826
|
|
|
task_name_key = 'context.chain.name' |
827
|
|
|
elif context and 'mistral' in context: |
828
|
|
|
task_name_key = 'context.mistral.task_name' |
829
|
|
|
# Use LiveAction as the object so that the formatter lookup does not change. |
830
|
|
|
# AKA HACK! |
831
|
|
|
return models.action.LiveAction(**{ |
832
|
|
|
'id': task.id, |
833
|
|
|
'status': task.status, |
834
|
|
|
'task': jsutil.get_value(vars(task), task_name_key), |
835
|
|
|
'action': task.action.get('ref', None), |
836
|
|
|
'start_timestamp': task.start_timestamp, |
837
|
|
|
'end_timestamp': getattr(task, 'end_timestamp', None) |
838
|
|
|
}) |
839
|
|
|
|
840
|
|
|
def _sort_parameters(self, parameters, names): |
841
|
|
|
""" |
842
|
|
|
Sort a provided list of action parameters. |
843
|
|
|
|
844
|
|
|
:type parameters: ``list`` |
845
|
|
|
:type names: ``list`` or ``set`` |
846
|
|
|
""" |
847
|
|
|
sorted_parameters = sorted(names, key=lambda name: |
848
|
|
|
self._get_parameter_sort_value( |
849
|
|
|
parameters=parameters, |
850
|
|
|
name=name)) |
851
|
|
|
|
852
|
|
|
return sorted_parameters |
853
|
|
|
|
854
|
|
|
def _get_parameter_sort_value(self, parameters, name): |
855
|
|
|
""" |
856
|
|
|
Return a value which determines sort order for a particular parameter. |
857
|
|
|
|
858
|
|
|
By default, parameters are sorted using "position" parameter attribute. |
859
|
|
|
If this attribute is not available, parameter is sorted based on the |
860
|
|
|
name. |
861
|
|
|
""" |
862
|
|
|
parameter = parameters.get(name, None) |
863
|
|
|
|
864
|
|
|
if not parameter: |
865
|
|
|
return None |
866
|
|
|
|
867
|
|
|
sort_value = parameter.get('position', name) |
868
|
|
|
return sort_value |
869
|
|
|
|
870
|
|
|
def _get_inherited_env_vars(self): |
871
|
|
|
env_vars = os.environ.copy() |
872
|
|
|
|
873
|
|
|
for var_name in ENV_VARS_BLACKLIST: |
874
|
|
|
if var_name.lower() in env_vars: |
875
|
|
|
del env_vars[var_name.lower()] |
876
|
|
|
if var_name.upper() in env_vars: |
877
|
|
|
del env_vars[var_name.upper()] |
878
|
|
|
|
879
|
|
|
return env_vars |
880
|
|
|
|
881
|
|
|
|
882
|
|
|
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
883
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
884
|
|
|
|
885
|
|
|
super(ActionRunCommand, self).__init__( |
886
|
|
|
resource, kwargs.pop('name', 'execute'), |
887
|
|
|
'A command to invoke an action manually.', |
888
|
|
|
*args, **kwargs) |
889
|
|
|
|
890
|
|
|
self.parser.add_argument('ref_or_id', nargs='?', |
891
|
|
|
metavar='ref-or-id', |
892
|
|
|
help='Action reference (pack.action_name) ' + |
893
|
|
|
'or ID of the action.') |
894
|
|
|
self.parser.add_argument('parameters', nargs='*', |
895
|
|
|
help='List of keyword args, positional args, ' |
896
|
|
|
'and optional args for the action.') |
897
|
|
|
|
898
|
|
|
self.parser.add_argument('-h', '--help', |
899
|
|
|
action='store_true', dest='help', |
900
|
|
|
help='Print usage for the given action.') |
901
|
|
|
|
902
|
|
|
self._add_common_options() |
903
|
|
|
|
904
|
|
|
if self.name in ['run', 'execute']: |
905
|
|
|
self.parser.add_argument('--trace-tag', '--trace_tag', |
906
|
|
|
help='A trace tag string to track execution later.', |
907
|
|
|
dest='trace_tag', required=False) |
908
|
|
|
self.parser.add_argument('--trace-id', |
909
|
|
|
help='Existing trace id for this execution.', |
910
|
|
|
dest='trace_id', required=False) |
911
|
|
|
self.parser.add_argument('-a', '--async', |
912
|
|
|
action='store_true', dest='async', |
913
|
|
|
help='Do not wait for action to finish.') |
914
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
915
|
|
|
action='store_true', dest='inherit_env', |
916
|
|
|
help='Pass all the environment variables ' |
917
|
|
|
'which are accessible to the CLI as "env" ' |
918
|
|
|
'parameter to the action. Note: Only works ' |
919
|
|
|
'with python, local and remote runners.') |
920
|
|
|
self.parser.add_argument('-u', '--user', type=str, default=None, |
921
|
|
|
help='User under which to run the action (admins only).') |
922
|
|
|
|
923
|
|
|
if self.name == 'run': |
924
|
|
|
self.parser.set_defaults(async=False) |
925
|
|
|
else: |
926
|
|
|
self.parser.set_defaults(async=True) |
927
|
|
|
|
928
|
|
|
@add_auth_token_to_kwargs_from_cli |
929
|
|
|
def run(self, args, **kwargs): |
930
|
|
|
if not args.ref_or_id: |
931
|
|
|
self.parser.error('Missing action reference or id') |
932
|
|
|
|
933
|
|
|
action = self.get_resource(args.ref_or_id, **kwargs) |
934
|
|
|
if not action: |
935
|
|
|
raise resource.ResourceNotFoundError('Action "%s" cannot be found.' |
936
|
|
|
% (args.ref_or_id)) |
937
|
|
|
|
938
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
939
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
940
|
|
|
if not runner: |
941
|
|
|
raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.' |
942
|
|
|
% (action.runner_type, action.name)) |
943
|
|
|
|
944
|
|
|
action_ref = '.'.join([action.pack, action.name]) |
945
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
946
|
|
|
args=args) |
947
|
|
|
|
948
|
|
|
execution = models.LiveAction() |
949
|
|
|
execution.action = action_ref |
950
|
|
|
execution.parameters = action_parameters |
951
|
|
|
execution.user = args.user |
952
|
|
|
|
953
|
|
|
if not args.trace_id and args.trace_tag: |
954
|
|
|
execution.context = {'trace_context': {'trace_tag': args.trace_tag}} |
955
|
|
|
|
956
|
|
|
if args.trace_id: |
957
|
|
|
execution.context = {'trace_context': {'id_': args.trace_id}} |
958
|
|
|
|
959
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
960
|
|
|
|
961
|
|
|
execution = action_exec_mgr.create(execution, **kwargs) |
962
|
|
|
execution = self._get_execution_result(execution=execution, |
963
|
|
|
action_exec_mgr=action_exec_mgr, |
964
|
|
|
args=args, **kwargs) |
965
|
|
|
return execution |
966
|
|
|
|
967
|
|
|
|
968
|
|
|
class ActionExecutionBranch(resource.ResourceBranch): |
969
|
|
|
|
970
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
971
|
|
|
super(ActionExecutionBranch, self).__init__( |
972
|
|
|
models.LiveAction, description, app, subparsers, |
973
|
|
|
parent_parser=parent_parser, read_only=True, |
974
|
|
|
commands={'list': ActionExecutionListCommand, |
975
|
|
|
'get': ActionExecutionGetCommand}) |
976
|
|
|
|
977
|
|
|
# Register extended commands |
978
|
|
|
self.commands['re-run'] = ActionExecutionReRunCommand( |
979
|
|
|
self.resource, self.app, self.subparsers, add_help=False) |
980
|
|
|
self.commands['cancel'] = ActionExecutionCancelCommand( |
981
|
|
|
self.resource, self.app, self.subparsers, add_help=False) |
982
|
|
|
self.commands['pause'] = ActionExecutionPauseCommand( |
983
|
|
|
self.resource, self.app, self.subparsers, add_help=False) |
984
|
|
|
self.commands['resume'] = ActionExecutionResumeCommand( |
985
|
|
|
self.resource, self.app, self.subparsers, add_help=False) |
986
|
|
|
|
987
|
|
|
|
988
|
|
|
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled') |
989
|
|
|
|
990
|
|
|
|
991
|
|
|
class ActionExecutionReadCommand(resource.ResourceCommand): |
992
|
|
|
""" |
993
|
|
|
Base class for read / view commands (list and get). |
994
|
|
|
""" |
995
|
|
|
|
996
|
|
|
@classmethod |
997
|
|
|
def _get_exclude_attributes(cls, args): |
998
|
|
|
""" |
999
|
|
|
Retrieve a list of exclude attributes for particular command line arguments. |
1000
|
|
|
""" |
1001
|
|
|
exclude_attributes = [] |
1002
|
|
|
|
1003
|
|
|
result_included = False |
1004
|
|
|
trigger_instance_included = False |
1005
|
|
|
|
1006
|
|
|
for attr in args.attr: |
1007
|
|
|
# Note: We perform startswith check so we correctly detected child attribute properties |
1008
|
|
|
# (e.g. result, result.stdout, result.stderr, etc.) |
1009
|
|
|
if attr.startswith('result'): |
1010
|
|
|
result_included = True |
1011
|
|
|
|
1012
|
|
|
if attr.startswith('trigger_instance'): |
1013
|
|
|
trigger_instance_included = True |
1014
|
|
|
|
1015
|
|
|
if not result_included: |
1016
|
|
|
exclude_attributes.append('result') |
1017
|
|
|
if not trigger_instance_included: |
1018
|
|
|
exclude_attributes.append('trigger_instance') |
1019
|
|
|
|
1020
|
|
|
return exclude_attributes |
1021
|
|
|
|
1022
|
|
|
|
1023
|
|
|
class ActionExecutionListCommand(ActionExecutionReadCommand): |
1024
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp', |
1025
|
|
|
'end_timestamp'] |
1026
|
|
|
attribute_transform_functions = { |
1027
|
|
|
'start_timestamp': format_isodate_for_user_timezone, |
1028
|
|
|
'end_timestamp': format_isodate_for_user_timezone, |
1029
|
|
|
'parameters': format_parameters, |
1030
|
|
|
'status': format_status |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1034
|
|
|
super(ActionExecutionListCommand, self).__init__( |
1035
|
|
|
resource, 'list', 'Get the list of the 50 most recent %s.' % |
1036
|
|
|
resource.get_plural_display_name().lower(), |
1037
|
|
|
*args, **kwargs) |
1038
|
|
|
|
1039
|
|
|
self.default_limit = 50 |
1040
|
|
|
self.resource_name = resource.get_plural_display_name().lower() |
1041
|
|
|
self.group = self.parser.add_argument_group() |
1042
|
|
|
self.parser.add_argument('-n', '--last', type=int, dest='last', |
1043
|
|
|
default=self.default_limit, |
1044
|
|
|
help=('List N most recent %s.' % self.resource_name)) |
1045
|
|
|
self.parser.add_argument('-s', '--sort', type=str, dest='sort_order', |
1046
|
|
|
default='descending', |
1047
|
|
|
help=('Sort %s by start timestamp, ' |
1048
|
|
|
'asc|ascending (earliest first) ' |
1049
|
|
|
'or desc|descending (latest first)' % self.resource_name)) |
1050
|
|
|
|
1051
|
|
|
# Filter options |
1052
|
|
|
self.group.add_argument('--action', help='Action reference to filter the list.') |
1053
|
|
|
self.group.add_argument('--status', help=('Only return executions with the provided status.' |
1054
|
|
|
' Possible values are \'%s\', \'%s\', \'%s\',' |
1055
|
|
|
'\'%s\' or \'%s\'' |
1056
|
|
|
'.' % POSSIBLE_ACTION_STATUS_VALUES)) |
1057
|
|
|
self.group.add_argument('--trigger_instance', |
1058
|
|
|
help='Trigger instance id to filter the list.') |
1059
|
|
|
self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt', |
1060
|
|
|
default=None, |
1061
|
|
|
help=('Only return executions with timestamp ' |
1062
|
|
|
'greater than the one provided. ' |
1063
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
1064
|
|
|
self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt', |
1065
|
|
|
default=None, |
1066
|
|
|
help=('Only return executions with timestamp ' |
1067
|
|
|
'lower than the one provided. ' |
1068
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
1069
|
|
|
self.parser.add_argument('-l', '--showall', action='store_true', |
1070
|
|
|
help='') |
1071
|
|
|
|
1072
|
|
|
# Display options |
1073
|
|
|
self.parser.add_argument('-a', '--attr', nargs='+', |
1074
|
|
|
default=self.display_attributes, |
1075
|
|
|
help=('List of attributes to include in the ' |
1076
|
|
|
'output. "all" will return all ' |
1077
|
|
|
'attributes.')) |
1078
|
|
|
self.parser.add_argument('-w', '--width', nargs='+', type=int, |
1079
|
|
|
default=None, |
1080
|
|
|
help=('Set the width of columns in output.')) |
1081
|
|
|
|
1082
|
|
|
@add_auth_token_to_kwargs_from_cli |
1083
|
|
|
def run(self, args, **kwargs): |
1084
|
|
|
# Filtering options |
1085
|
|
|
if args.action: |
1086
|
|
|
kwargs['action'] = args.action |
1087
|
|
|
if args.status: |
1088
|
|
|
kwargs['status'] = args.status |
1089
|
|
|
if args.trigger_instance: |
1090
|
|
|
kwargs['trigger_instance'] = args.trigger_instance |
1091
|
|
|
if not args.showall: |
1092
|
|
|
# null is the magic string that translates to does not exist. |
1093
|
|
|
kwargs['parent'] = 'null' |
1094
|
|
|
if args.timestamp_gt: |
1095
|
|
|
kwargs['timestamp_gt'] = args.timestamp_gt |
1096
|
|
|
if args.timestamp_lt: |
1097
|
|
|
kwargs['timestamp_lt'] = args.timestamp_lt |
1098
|
|
|
if args.sort_order: |
1099
|
|
|
if args.sort_order in ['asc', 'ascending']: |
1100
|
|
|
kwargs['sort_asc'] = True |
1101
|
|
|
elif args.sort_order in ['desc', 'descending']: |
1102
|
|
|
kwargs['sort_desc'] = True |
1103
|
|
|
|
1104
|
|
|
# We exclude "result" and "trigger_instance" attributes which can contain a lot of data |
1105
|
|
|
# since they are not displayed nor used which speeds the common operation substantially. |
1106
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
1107
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
1108
|
|
|
kwargs['exclude_attributes'] = exclude_attributes |
1109
|
|
|
|
1110
|
|
|
return self.manager.query_with_count(limit=args.last, **kwargs) |
1111
|
|
|
|
1112
|
|
|
def run_and_print(self, args, **kwargs): |
1113
|
|
|
|
1114
|
|
|
result, count = self.run(args, **kwargs) |
1115
|
|
|
instances = format_wf_instances(result) |
1116
|
|
|
|
1117
|
|
|
if args.json or args.yaml: |
1118
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
1119
|
|
|
attributes=args.attr, widths=args.width, |
1120
|
|
|
json=args.json, |
1121
|
|
|
yaml=args.yaml, |
1122
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
1123
|
|
|
|
1124
|
|
|
else: |
1125
|
|
|
# Include elapsed time for running executions |
1126
|
|
|
instances = format_execution_statuses(instances) |
1127
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
1128
|
|
|
attributes=args.attr, widths=args.width, |
1129
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
1130
|
|
|
|
1131
|
|
|
if args.last and count and count > args.last: |
1132
|
|
|
table.SingleRowTable.note_box(self.resource_name, args.last) |
1133
|
|
|
|
1134
|
|
|
|
1135
|
|
|
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1136
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1137
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1138
|
|
|
|
1139
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1140
|
|
|
super(ActionExecutionGetCommand, self).__init__( |
1141
|
|
|
resource, 'get', |
1142
|
|
|
'Get individual %s.' % resource.get_display_name().lower(), |
1143
|
|
|
*args, **kwargs) |
1144
|
|
|
|
1145
|
|
|
self.parser.add_argument('id', |
1146
|
|
|
help=('ID of the %s.' % |
1147
|
|
|
resource.get_display_name().lower())) |
1148
|
|
|
|
1149
|
|
|
self._add_common_options() |
1150
|
|
|
|
1151
|
|
|
@add_auth_token_to_kwargs_from_cli |
1152
|
|
|
def run(self, args, **kwargs): |
1153
|
|
|
# We exclude "result" and / or "trigger_instance" attribute if it's not explicitly |
1154
|
|
|
# requested by user either via "--attr" flag or by default. |
1155
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
1156
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
1157
|
|
|
|
1158
|
|
|
kwargs['params'] = {'exclude_attributes': exclude_attributes} |
1159
|
|
|
|
1160
|
|
|
execution = self.get_resource_by_id(id=args.id, **kwargs) |
1161
|
|
|
return execution |
1162
|
|
|
|
1163
|
|
|
@add_auth_token_to_kwargs_from_cli |
1164
|
|
|
def run_and_print(self, args, **kwargs): |
1165
|
|
|
try: |
1166
|
|
|
execution = self.run(args, **kwargs) |
1167
|
|
|
|
1168
|
|
|
if not args.json and not args.yaml: |
1169
|
|
|
# Include elapsed time for running executions |
1170
|
|
|
execution = format_execution_status(execution) |
1171
|
|
|
except resource.ResourceNotFoundError: |
1172
|
|
|
self.print_not_found(args.id) |
1173
|
|
|
raise OperationFailureException('Execution %s not found.' % (args.id)) |
1174
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1175
|
|
|
|
1176
|
|
|
|
1177
|
|
|
class ActionExecutionCancelCommand(resource.ResourceCommand): |
1178
|
|
|
|
1179
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1180
|
|
|
super(ActionExecutionCancelCommand, self).__init__( |
1181
|
|
|
resource, 'cancel', 'Cancel %s.' % |
1182
|
|
|
resource.get_plural_display_name().lower(), |
1183
|
|
|
*args, **kwargs) |
1184
|
|
|
|
1185
|
|
|
self.parser.add_argument('ids', |
1186
|
|
|
nargs='+', |
1187
|
|
|
help=('IDs of the %ss to cancel.' % |
1188
|
|
|
resource.get_display_name().lower())) |
1189
|
|
|
|
1190
|
|
|
def run(self, args, **kwargs): |
1191
|
|
|
responses = [] |
1192
|
|
|
for execution_id in args.ids: |
1193
|
|
|
response = self.manager.delete_by_id(execution_id) |
1194
|
|
|
responses.append([execution_id, response]) |
1195
|
|
|
|
1196
|
|
|
return responses |
1197
|
|
|
|
1198
|
|
|
@add_auth_token_to_kwargs_from_cli |
1199
|
|
|
def run_and_print(self, args, **kwargs): |
1200
|
|
|
responses = self.run(args, **kwargs) |
1201
|
|
|
|
1202
|
|
|
for execution_id, response in responses: |
1203
|
|
|
self._print_result(execution_id=execution_id, response=response) |
1204
|
|
|
|
1205
|
|
|
def _print_result(self, execution_id, response): |
1206
|
|
|
if response and 'faultstring' in response: |
1207
|
|
|
message = response.get('faultstring', 'Cancellation requested for %s with id %s.' % |
1208
|
|
|
(self.resource.get_display_name().lower(), execution_id)) |
1209
|
|
|
|
1210
|
|
|
elif response: |
1211
|
|
|
message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(), |
1212
|
|
|
execution_id) |
1213
|
|
|
else: |
1214
|
|
|
message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(), |
1215
|
|
|
execution_id) |
1216
|
|
|
print(message) |
1217
|
|
|
|
1218
|
|
|
|
1219
|
|
|
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
1220
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1221
|
|
|
|
1222
|
|
|
super(ActionExecutionReRunCommand, self).__init__( |
1223
|
|
|
resource, kwargs.pop('name', 're-run'), |
1224
|
|
|
'A command to re-run a particular action.', |
1225
|
|
|
*args, **kwargs) |
1226
|
|
|
|
1227
|
|
|
self.parser.add_argument('id', nargs='?', |
1228
|
|
|
metavar='id', |
1229
|
|
|
help='ID of action execution to re-run ') |
1230
|
|
|
self.parser.add_argument('parameters', nargs='*', |
1231
|
|
|
help='List of keyword args, positional args, ' |
1232
|
|
|
'and optional args for the action.') |
1233
|
|
|
self.parser.add_argument('--tasks', nargs='*', |
1234
|
|
|
help='Name of the workflow tasks to re-run.') |
1235
|
|
|
self.parser.add_argument('--no-reset', dest='no_reset', nargs='*', |
1236
|
|
|
help='Name of the with-items tasks to not reset. This only ' |
1237
|
|
|
'applies to Mistral workflows. By default, all iterations ' |
1238
|
|
|
'for with-items tasks is rerun. If no reset, only failed ' |
1239
|
|
|
' iterations are rerun.') |
1240
|
|
|
self.parser.add_argument('-a', '--async', |
1241
|
|
|
action='store_true', dest='async', |
1242
|
|
|
help='Do not wait for action to finish.') |
1243
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
1244
|
|
|
action='store_true', dest='inherit_env', |
1245
|
|
|
help='Pass all the environment variables ' |
1246
|
|
|
'which are accessible to the CLI as "env" ' |
1247
|
|
|
'parameter to the action. Note: Only works ' |
1248
|
|
|
'with python, local and remote runners.') |
1249
|
|
|
self.parser.add_argument('-h', '--help', |
1250
|
|
|
action='store_true', dest='help', |
1251
|
|
|
help='Print usage for the given action.') |
1252
|
|
|
|
1253
|
|
|
self._add_common_options() |
1254
|
|
|
|
1255
|
|
|
@add_auth_token_to_kwargs_from_cli |
1256
|
|
|
def run(self, args, **kwargs): |
1257
|
|
|
existing_execution = self.manager.get_by_id(args.id, **kwargs) |
1258
|
|
|
|
1259
|
|
|
if not existing_execution: |
1260
|
|
|
raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' % |
1261
|
|
|
(args.id)) |
1262
|
|
|
|
1263
|
|
|
action_mgr = self.app.client.managers['Action'] |
1264
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
1265
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
1266
|
|
|
|
1267
|
|
|
action_ref = existing_execution.action['ref'] |
1268
|
|
|
action = action_mgr.get_by_ref_or_id(action_ref) |
1269
|
|
|
runner = runner_mgr.get_by_name(action.runner_type) |
1270
|
|
|
|
1271
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
1272
|
|
|
args=args) |
1273
|
|
|
|
1274
|
|
|
execution = action_exec_mgr.re_run(execution_id=args.id, |
1275
|
|
|
parameters=action_parameters, |
1276
|
|
|
tasks=args.tasks, |
1277
|
|
|
no_reset=args.no_reset, |
1278
|
|
|
**kwargs) |
1279
|
|
|
|
1280
|
|
|
execution = self._get_execution_result(execution=execution, |
1281
|
|
|
action_exec_mgr=action_exec_mgr, |
1282
|
|
|
args=args, **kwargs) |
1283
|
|
|
|
1284
|
|
|
return execution |
1285
|
|
|
|
1286
|
|
|
|
1287
|
|
|
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1288
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1289
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1290
|
|
|
|
1291
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1292
|
|
|
super(ActionExecutionPauseCommand, self).__init__( |
1293
|
|
|
resource, 'pause', 'Pause %s (workflow executions only).' % |
1294
|
|
|
resource.get_plural_display_name().lower(), |
1295
|
|
|
*args, **kwargs) |
1296
|
|
|
|
1297
|
|
|
self.parser.add_argument('id', nargs='?', |
1298
|
|
|
metavar='id', |
1299
|
|
|
help='ID of action execution to pause.') |
1300
|
|
|
|
1301
|
|
|
self._add_common_options() |
1302
|
|
|
|
1303
|
|
|
@add_auth_token_to_kwargs_from_cli |
1304
|
|
|
def run(self, args, **kwargs): |
1305
|
|
|
return self.manager.pause(args.id) |
1306
|
|
|
|
1307
|
|
|
@add_auth_token_to_kwargs_from_cli |
1308
|
|
|
def run_and_print(self, args, **kwargs): |
1309
|
|
|
try: |
1310
|
|
|
execution = self.run(args, **kwargs) |
1311
|
|
|
|
1312
|
|
|
if not args.json and not args.yaml: |
1313
|
|
|
# Include elapsed time for running executions |
1314
|
|
|
execution = format_execution_status(execution) |
1315
|
|
|
except resource.ResourceNotFoundError: |
1316
|
|
|
self.print_not_found(args.id) |
1317
|
|
|
raise OperationFailureException('Execution %s not found.' % (args.id)) |
1318
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1319
|
|
|
|
1320
|
|
|
|
1321
|
|
|
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1322
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1323
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1324
|
|
|
|
1325
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1326
|
|
|
super(ActionExecutionResumeCommand, self).__init__( |
1327
|
|
|
resource, 'resume', 'Resume %s (workflow executions only).' % |
1328
|
|
|
resource.get_plural_display_name().lower(), |
1329
|
|
|
*args, **kwargs) |
1330
|
|
|
|
1331
|
|
|
self.parser.add_argument('id', nargs='?', |
1332
|
|
|
metavar='id', |
1333
|
|
|
help='ID of action execution to resume.') |
1334
|
|
|
|
1335
|
|
|
self._add_common_options() |
1336
|
|
|
|
1337
|
|
|
@add_auth_token_to_kwargs_from_cli |
1338
|
|
|
def run(self, args, **kwargs): |
1339
|
|
|
return self.manager.resume(args.id) |
1340
|
|
|
|
1341
|
|
|
@add_auth_token_to_kwargs_from_cli |
1342
|
|
|
def run_and_print(self, args, **kwargs): |
1343
|
|
|
try: |
1344
|
|
|
execution = self.run(args, **kwargs) |
1345
|
|
|
|
1346
|
|
|
if not args.json and not args.yaml: |
1347
|
|
|
# Include elapsed time for running executions |
1348
|
|
|
execution = format_execution_status(execution) |
1349
|
|
|
except resource.ResourceNotFoundError: |
1350
|
|
|
self.print_not_found(args.id) |
1351
|
|
|
raise OperationFailureException('Execution %s not found.' % (args.id)) |
1352
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1353
|
|
|
|
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: