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
|
|
|
|
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_for_user_timezone, |
239
|
|
|
'end_timestamp': format_isodate_for_user_timezone, |
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
|
|
|
yaml=args.yaml, |
399
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
400
|
|
|
|
401
|
|
|
def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): |
402
|
|
|
pending_statuses = [ |
403
|
|
|
LIVEACTION_STATUS_REQUESTED, |
404
|
|
|
LIVEACTION_STATUS_SCHEDULED, |
405
|
|
|
LIVEACTION_STATUS_RUNNING, |
406
|
|
|
LIVEACTION_STATUS_CANCELING |
407
|
|
|
] |
408
|
|
|
|
409
|
|
|
if not args.async: |
410
|
|
|
while execution.status in pending_statuses: |
411
|
|
|
time.sleep(self.poll_interval) |
412
|
|
|
if not args.json and not args.yaml: |
413
|
|
|
sys.stdout.write('.') |
414
|
|
|
sys.stdout.flush() |
415
|
|
|
execution = action_exec_mgr.get_by_id(execution.id, **kwargs) |
416
|
|
|
|
417
|
|
|
sys.stdout.write('\n') |
418
|
|
|
|
419
|
|
|
if execution.status == LIVEACTION_STATUS_CANCELED: |
420
|
|
|
return execution |
421
|
|
|
|
422
|
|
|
return execution |
423
|
|
|
|
424
|
|
|
def _get_top_level_error(self, live_action): |
425
|
|
|
""" |
426
|
|
|
Retrieve a top level workflow error. |
427
|
|
|
|
428
|
|
|
:return: (error, traceback) |
429
|
|
|
""" |
430
|
|
|
if isinstance(live_action.result, dict): |
431
|
|
|
error = live_action.result.get('error', None) |
432
|
|
|
traceback = live_action.result.get('traceback', None) |
433
|
|
|
else: |
434
|
|
|
error = "See result" |
435
|
|
|
traceback = "See result" |
436
|
|
|
|
437
|
|
|
return error, traceback |
438
|
|
|
|
439
|
|
|
def _get_task_error(self, task): |
440
|
|
|
""" |
441
|
|
|
Retrieve error message from the provided task. |
442
|
|
|
|
443
|
|
|
:return: (error, traceback) |
444
|
|
|
""" |
445
|
|
|
if not task: |
446
|
|
|
return None, None |
447
|
|
|
|
448
|
|
|
result = task['result'] |
449
|
|
|
|
450
|
|
|
if isinstance(result, dict): |
451
|
|
|
stderr = result.get('stderr', None) |
452
|
|
|
error = result.get('error', None) |
453
|
|
|
traceback = result.get('traceback', None) |
454
|
|
|
error = error if error else stderr |
455
|
|
|
else: |
456
|
|
|
stderr = None |
457
|
|
|
error = None |
458
|
|
|
traceback = None |
459
|
|
|
|
460
|
|
|
return error, traceback |
461
|
|
|
|
462
|
|
|
def _get_action_parameters_from_args(self, action, runner, args): |
463
|
|
|
""" |
464
|
|
|
Build a dictionary with parameters which will be passed to the action by |
465
|
|
|
parsing parameters passed to the CLI. |
466
|
|
|
|
467
|
|
|
:param args: CLI argument. |
468
|
|
|
:type args: ``object`` |
469
|
|
|
|
470
|
|
|
:rtype: ``dict`` |
471
|
|
|
""" |
472
|
|
|
action_ref_or_id = action.ref |
473
|
|
|
|
474
|
|
|
def read_file(file_path): |
475
|
|
|
if not os.path.exists(file_path): |
476
|
|
|
raise ValueError('File "%s" doesn\'t exist' % (file_path)) |
477
|
|
|
|
478
|
|
|
if not os.path.isfile(file_path): |
479
|
|
|
raise ValueError('"%s" is not a file' % (file_path)) |
480
|
|
|
|
481
|
|
|
with open(file_path, 'rb') as fp: |
482
|
|
|
content = fp.read() |
483
|
|
|
|
484
|
|
|
return content |
485
|
|
|
|
486
|
|
|
def transform_object(value): |
487
|
|
|
# Also support simple key1=val1,key2=val2 syntax |
488
|
|
|
if value.startswith('{'): |
489
|
|
|
# Assume it's JSON |
490
|
|
|
result = value = json.loads(value) |
491
|
|
|
else: |
492
|
|
|
pairs = value.split(',') |
493
|
|
|
|
494
|
|
|
result = {} |
495
|
|
|
for pair in pairs: |
496
|
|
|
split = pair.split('=', 1) |
497
|
|
|
|
498
|
|
|
if len(split) != 2: |
499
|
|
|
continue |
500
|
|
|
|
501
|
|
|
key, value = split |
502
|
|
|
result[key] = value |
503
|
|
|
return result |
504
|
|
|
|
505
|
|
|
transformer = { |
506
|
|
|
'array': (lambda cs_x: [v.strip() for v in cs_x.split(',')]), |
507
|
|
|
'boolean': (lambda x: ast.literal_eval(x.capitalize())), |
508
|
|
|
'integer': int, |
509
|
|
|
'number': float, |
510
|
|
|
'object': transform_object, |
511
|
|
|
'string': str |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
def normalize(name, value): |
515
|
|
|
if name in runner.runner_parameters: |
516
|
|
|
param = runner.runner_parameters[name] |
517
|
|
|
if 'type' in param and param['type'] in transformer: |
518
|
|
|
return transformer[param['type']](value) |
519
|
|
|
|
520
|
|
|
if name in action.parameters: |
521
|
|
|
param = action.parameters[name] |
522
|
|
|
if 'type' in param and param['type'] in transformer: |
523
|
|
|
return transformer[param['type']](value) |
524
|
|
|
return value |
525
|
|
|
|
526
|
|
|
result = {} |
527
|
|
|
|
528
|
|
|
if not args.parameters: |
529
|
|
|
return result |
530
|
|
|
|
531
|
|
|
for idx in range(len(args.parameters)): |
532
|
|
|
arg = args.parameters[idx] |
533
|
|
|
if '=' in arg: |
534
|
|
|
k, v = arg.split('=', 1) |
535
|
|
|
|
536
|
|
|
# Attribute for files are prefixed with "@" |
537
|
|
|
if k.startswith('@'): |
538
|
|
|
k = k[1:] |
539
|
|
|
is_file = True |
540
|
|
|
else: |
541
|
|
|
is_file = False |
542
|
|
|
|
543
|
|
|
try: |
544
|
|
|
if is_file: |
545
|
|
|
# Files are handled a bit differently since we ship the content |
546
|
|
|
# over the wire |
547
|
|
|
file_path = os.path.normpath(pjoin(os.getcwd(), v)) |
548
|
|
|
file_name = os.path.basename(file_path) |
549
|
|
|
content = read_file(file_path=file_path) |
550
|
|
|
|
551
|
|
|
if action_ref_or_id == 'core.http': |
552
|
|
|
# Special case for http runner |
553
|
|
|
result['_file_name'] = file_name |
554
|
|
|
result['file_content'] = content |
555
|
|
|
else: |
556
|
|
|
result[k] = content |
557
|
|
|
else: |
558
|
|
|
result[k] = normalize(k, v) |
559
|
|
|
except Exception as e: |
560
|
|
|
# TODO: Move transformers in a separate module and handle |
561
|
|
|
# exceptions there |
562
|
|
|
if 'malformed string' in str(e): |
563
|
|
|
message = ('Invalid value for boolean parameter. ' |
564
|
|
|
'Valid values are: true, false') |
565
|
|
|
raise ValueError(message) |
566
|
|
|
else: |
567
|
|
|
raise e |
568
|
|
|
else: |
569
|
|
|
result['cmd'] = ' '.join(args.parameters[idx:]) |
570
|
|
|
break |
571
|
|
|
|
572
|
|
|
# Special case for http runner |
573
|
|
|
if 'file_content' in result: |
574
|
|
|
if 'method' not in result: |
575
|
|
|
# Default to POST if a method is not provided |
576
|
|
|
result['method'] = 'POST' |
577
|
|
|
|
578
|
|
|
if 'file_name' not in result: |
579
|
|
|
# File name not provided, use default file name |
580
|
|
|
result['file_name'] = result['_file_name'] |
581
|
|
|
|
582
|
|
|
del result['_file_name'] |
583
|
|
|
|
584
|
|
|
if args.inherit_env: |
585
|
|
|
result['env'] = self._get_inherited_env_vars() |
586
|
|
|
|
587
|
|
|
return result |
588
|
|
|
|
589
|
|
|
@add_auth_token_to_kwargs_from_cli |
590
|
|
|
def _print_help(self, args, **kwargs): |
591
|
|
|
# Print appropriate help message if the help option is given. |
592
|
|
|
action_mgr = self.app.client.managers['Action'] |
593
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
594
|
|
|
|
595
|
|
|
if args.help: |
596
|
|
|
action_ref_or_id = getattr(args, 'ref_or_id', None) |
597
|
|
|
action_exec_id = getattr(args, 'id', None) |
598
|
|
|
|
599
|
|
|
if action_exec_id and not action_ref_or_id: |
600
|
|
|
action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs) |
601
|
|
|
args.ref_or_id = action_exec.action |
602
|
|
|
|
603
|
|
|
if action_ref_or_id: |
604
|
|
|
try: |
605
|
|
|
action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs) |
606
|
|
|
if not action: |
607
|
|
|
raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id) |
608
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
609
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
610
|
|
|
parameters, required, optional, _ = self._get_params_types(runner, |
611
|
|
|
action) |
612
|
|
|
print('') |
613
|
|
|
print(textwrap.fill(action.description)) |
614
|
|
|
print('') |
615
|
|
|
if required: |
616
|
|
|
required = self._sort_parameters(parameters=parameters, |
617
|
|
|
names=required) |
618
|
|
|
|
619
|
|
|
print('Required Parameters:') |
620
|
|
|
[self._print_param(name, parameters.get(name)) |
621
|
|
|
for name in required] |
622
|
|
|
if optional: |
623
|
|
|
optional = self._sort_parameters(parameters=parameters, |
624
|
|
|
names=optional) |
625
|
|
|
|
626
|
|
|
print('Optional Parameters:') |
627
|
|
|
[self._print_param(name, parameters.get(name)) |
628
|
|
|
for name in optional] |
629
|
|
|
except resource.ResourceNotFoundError: |
630
|
|
|
print(('Action "%s" is not found. ' % args.ref_or_id) + |
631
|
|
|
'Do "st2 action list" to see list of available actions.') |
632
|
|
|
except Exception as e: |
633
|
|
|
print('ERROR: Unable to print help for action "%s". %s' % |
634
|
|
|
(args.ref_or_id, e)) |
635
|
|
|
else: |
636
|
|
|
self.parser.print_help() |
637
|
|
|
return True |
638
|
|
|
return False |
639
|
|
|
|
640
|
|
|
@staticmethod |
641
|
|
|
def _print_param(name, schema): |
642
|
|
|
if not schema: |
643
|
|
|
raise ValueError('Missing schema for parameter "%s"' % (name)) |
644
|
|
|
|
645
|
|
|
wrapper = textwrap.TextWrapper(width=78) |
646
|
|
|
wrapper.initial_indent = ' ' * 4 |
647
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
648
|
|
|
print(wrapper.fill(name)) |
649
|
|
|
wrapper.initial_indent = ' ' * 8 |
650
|
|
|
wrapper.subsequent_indent = wrapper.initial_indent |
651
|
|
|
if 'description' in schema and schema['description']: |
652
|
|
|
print(wrapper.fill(schema['description'])) |
653
|
|
|
if 'type' in schema and schema['type']: |
654
|
|
|
print(wrapper.fill('Type: %s' % schema['type'])) |
655
|
|
|
if 'enum' in schema and schema['enum']: |
656
|
|
|
print(wrapper.fill('Enum: %s' % ', '.join(schema['enum']))) |
657
|
|
|
if 'default' in schema and schema['default'] is not None: |
658
|
|
|
print(wrapper.fill('Default: %s' % schema['default'])) |
659
|
|
|
print('') |
660
|
|
|
|
661
|
|
|
@staticmethod |
662
|
|
|
def _get_params_types(runner, action): |
663
|
|
|
runner_params = runner.runner_parameters |
664
|
|
|
action_params = action.parameters |
665
|
|
|
parameters = copy.copy(runner_params) |
666
|
|
|
parameters.update(copy.copy(action_params)) |
667
|
|
|
required = set([k for k, v in six.iteritems(parameters) if v.get('required')]) |
668
|
|
|
|
669
|
|
|
def is_immutable(runner_param_meta, action_param_meta): |
670
|
|
|
# If runner sets a param as immutable, action cannot override that. |
671
|
|
|
if runner_param_meta.get('immutable', False): |
672
|
|
|
return True |
673
|
|
|
else: |
674
|
|
|
return action_param_meta.get('immutable', False) |
675
|
|
|
|
676
|
|
|
immutable = set() |
677
|
|
|
for param in parameters.keys(): |
678
|
|
|
if is_immutable(runner_params.get(param, {}), |
679
|
|
|
action_params.get(param, {})): |
680
|
|
|
immutable.add(param) |
681
|
|
|
|
682
|
|
|
required = required - immutable |
683
|
|
|
optional = set(parameters.keys()) - required - immutable |
684
|
|
|
|
685
|
|
|
return parameters, required, optional, immutable |
686
|
|
|
|
687
|
|
|
def _format_child_instances(self, children, parent_id): |
688
|
|
|
''' |
689
|
|
|
The goal of this method is to add an indent at every level. This way the |
690
|
|
|
WF is represented as a tree structure while in a list. For the right visuals |
691
|
|
|
representation the list must be a DF traversal else the idents will end up |
692
|
|
|
looking strange. |
693
|
|
|
''' |
694
|
|
|
# apply basic WF formating first. |
695
|
|
|
children = format_wf_instances(children) |
696
|
|
|
# setup a depth lookup table |
697
|
|
|
depth = {parent_id: 0} |
698
|
|
|
result = [] |
699
|
|
|
# main loop that indents each entry correctly |
700
|
|
|
for child in children: |
701
|
|
|
# make sure child.parent is in depth and while at it compute the |
702
|
|
|
# right depth for indentation purposes. |
703
|
|
|
if child.parent not in depth: |
704
|
|
|
parent = None |
705
|
|
|
for instance in children: |
706
|
|
|
if WF_PREFIX in instance.id: |
707
|
|
|
instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):] |
708
|
|
|
else: |
709
|
|
|
instance_id = instance.id |
710
|
|
|
if instance_id == child.parent: |
711
|
|
|
parent = instance |
712
|
|
|
if parent and parent.parent and parent.parent in depth: |
713
|
|
|
depth[child.parent] = depth[parent.parent] + 1 |
714
|
|
|
else: |
715
|
|
|
depth[child.parent] = 0 |
716
|
|
|
# now ident for the right visuals |
717
|
|
|
child.id = INDENT_CHAR * depth[child.parent] + child.id |
718
|
|
|
result.append(self._format_for_common_representation(child)) |
719
|
|
|
return result |
720
|
|
|
|
721
|
|
|
def _format_for_common_representation(self, task): |
722
|
|
|
''' |
723
|
|
|
Formats a task for common representation between mistral and action-chain. |
724
|
|
|
''' |
725
|
|
|
# This really needs to be better handled on the back-end but that would be a bigger |
726
|
|
|
# change so handling in cli. |
727
|
|
|
context = getattr(task, 'context', None) |
728
|
|
|
if context and 'chain' in context: |
729
|
|
|
task_name_key = 'context.chain.name' |
730
|
|
|
elif context and 'mistral' in context: |
731
|
|
|
task_name_key = 'context.mistral.task_name' |
732
|
|
|
# Use LiveAction as the object so that the formatter lookup does not change. |
733
|
|
|
# AKA HACK! |
734
|
|
|
return models.action.LiveAction(**{ |
735
|
|
|
'id': task.id, |
736
|
|
|
'status': task.status, |
737
|
|
|
'task': jsutil.get_value(vars(task), task_name_key), |
738
|
|
|
'action': task.action.get('ref', None), |
739
|
|
|
'start_timestamp': task.start_timestamp, |
740
|
|
|
'end_timestamp': getattr(task, 'end_timestamp', None) |
741
|
|
|
}) |
742
|
|
|
|
743
|
|
|
def _sort_parameters(self, parameters, names): |
744
|
|
|
""" |
745
|
|
|
Sort a provided list of action parameters. |
746
|
|
|
|
747
|
|
|
:type parameters: ``list`` |
748
|
|
|
:type names: ``list`` or ``set`` |
749
|
|
|
""" |
750
|
|
|
sorted_parameters = sorted(names, key=lambda name: |
751
|
|
|
self._get_parameter_sort_value( |
752
|
|
|
parameters=parameters, |
753
|
|
|
name=name)) |
754
|
|
|
|
755
|
|
|
return sorted_parameters |
756
|
|
|
|
757
|
|
|
def _get_parameter_sort_value(self, parameters, name): |
758
|
|
|
""" |
759
|
|
|
Return a value which determines sort order for a particular parameter. |
760
|
|
|
|
761
|
|
|
By default, parameters are sorted using "position" parameter attribute. |
762
|
|
|
If this attribute is not available, parameter is sorted based on the |
763
|
|
|
name. |
764
|
|
|
""" |
765
|
|
|
parameter = parameters.get(name, None) |
766
|
|
|
|
767
|
|
|
if not parameter: |
768
|
|
|
return None |
769
|
|
|
|
770
|
|
|
sort_value = parameter.get('position', name) |
771
|
|
|
return sort_value |
772
|
|
|
|
773
|
|
|
def _get_inherited_env_vars(self): |
774
|
|
|
env_vars = os.environ.copy() |
775
|
|
|
|
776
|
|
|
for var_name in ENV_VARS_BLACKLIST: |
777
|
|
|
if var_name.lower() in env_vars: |
778
|
|
|
del env_vars[var_name.lower()] |
779
|
|
|
if var_name.upper() in env_vars: |
780
|
|
|
del env_vars[var_name.upper()] |
781
|
|
|
|
782
|
|
|
return env_vars |
783
|
|
|
|
784
|
|
|
|
785
|
|
|
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
786
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
787
|
|
|
|
788
|
|
|
super(ActionRunCommand, self).__init__( |
789
|
|
|
resource, kwargs.pop('name', 'execute'), |
790
|
|
|
'A command to invoke an action manually.', |
791
|
|
|
*args, **kwargs) |
792
|
|
|
|
793
|
|
|
self.parser.add_argument('ref_or_id', nargs='?', |
794
|
|
|
metavar='ref-or-id', |
795
|
|
|
help='Action reference (pack.action_name) ' + |
796
|
|
|
'or ID of the action.') |
797
|
|
|
self.parser.add_argument('parameters', nargs='*', |
798
|
|
|
help='List of keyword args, positional args, ' |
799
|
|
|
'and optional args for the action.') |
800
|
|
|
|
801
|
|
|
self.parser.add_argument('-h', '--help', |
802
|
|
|
action='store_true', dest='help', |
803
|
|
|
help='Print usage for the given action.') |
804
|
|
|
|
805
|
|
|
self._add_common_options() |
806
|
|
|
|
807
|
|
|
if self.name in ['run', 'execute']: |
808
|
|
|
self.parser.add_argument('--trace-tag', '--trace_tag', |
809
|
|
|
help='A trace tag string to track execution later.', |
810
|
|
|
dest='trace_tag', required=False) |
811
|
|
|
self.parser.add_argument('--trace-id', |
812
|
|
|
help='Existing trace id for this execution.', |
813
|
|
|
dest='trace_id', required=False) |
814
|
|
|
self.parser.add_argument('-a', '--async', |
815
|
|
|
action='store_true', dest='async', |
816
|
|
|
help='Do not wait for action to finish.') |
817
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
818
|
|
|
action='store_true', dest='inherit_env', |
819
|
|
|
help='Pass all the environment variables ' |
820
|
|
|
'which are accessible to the CLI as "env" ' |
821
|
|
|
'parameter to the action. Note: Only works ' |
822
|
|
|
'with python, local and remote runners.') |
823
|
|
|
self.parser.add_argument('-u', '--user', type=str, default=None, |
824
|
|
|
help='User under which to run the action (admins only).') |
825
|
|
|
|
826
|
|
|
if self.name == 'run': |
827
|
|
|
self.parser.set_defaults(async=False) |
828
|
|
|
else: |
829
|
|
|
self.parser.set_defaults(async=True) |
830
|
|
|
|
831
|
|
|
@add_auth_token_to_kwargs_from_cli |
832
|
|
|
def run(self, args, **kwargs): |
833
|
|
|
if not args.ref_or_id: |
834
|
|
|
self.parser.error('Missing action reference or id') |
835
|
|
|
|
836
|
|
|
action = self.get_resource(args.ref_or_id, **kwargs) |
837
|
|
|
if not action: |
838
|
|
|
raise resource.ResourceNotFoundError('Action "%s" cannot be found.' |
839
|
|
|
% (args.ref_or_id)) |
840
|
|
|
|
841
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
842
|
|
|
runner = runner_mgr.get_by_name(action.runner_type, **kwargs) |
843
|
|
|
if not runner: |
844
|
|
|
raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.' |
845
|
|
|
% (action.runner_type, action.name)) |
846
|
|
|
|
847
|
|
|
action_ref = '.'.join([action.pack, action.name]) |
848
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
849
|
|
|
args=args) |
850
|
|
|
|
851
|
|
|
execution = models.LiveAction() |
852
|
|
|
execution.action = action_ref |
853
|
|
|
execution.parameters = action_parameters |
854
|
|
|
execution.user = args.user |
855
|
|
|
|
856
|
|
|
if not args.trace_id and args.trace_tag: |
857
|
|
|
execution.context = {'trace_context': {'trace_tag': args.trace_tag}} |
858
|
|
|
|
859
|
|
|
if args.trace_id: |
860
|
|
|
execution.context = {'trace_context': {'id_': args.trace_id}} |
861
|
|
|
|
862
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
863
|
|
|
|
864
|
|
|
execution = action_exec_mgr.create(execution, **kwargs) |
865
|
|
|
execution = self._get_execution_result(execution=execution, |
866
|
|
|
action_exec_mgr=action_exec_mgr, |
867
|
|
|
args=args, **kwargs) |
868
|
|
|
return execution |
869
|
|
|
|
870
|
|
|
|
871
|
|
|
class ActionExecutionBranch(resource.ResourceBranch): |
872
|
|
|
|
873
|
|
|
def __init__(self, description, app, subparsers, parent_parser=None): |
874
|
|
|
super(ActionExecutionBranch, self).__init__( |
875
|
|
|
models.LiveAction, description, app, subparsers, |
876
|
|
|
parent_parser=parent_parser, read_only=True, |
877
|
|
|
commands={'list': ActionExecutionListCommand, |
878
|
|
|
'get': ActionExecutionGetCommand}) |
879
|
|
|
|
880
|
|
|
# Register extended commands |
881
|
|
|
self.commands['re-run'] = ActionExecutionReRunCommand(self.resource, self.app, |
882
|
|
|
self.subparsers, add_help=False) |
883
|
|
|
self.commands['cancel'] = ActionExecutionCancelCommand(self.resource, self.app, |
884
|
|
|
self.subparsers, add_help=False) |
885
|
|
|
|
886
|
|
|
|
887
|
|
|
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled') |
888
|
|
|
|
889
|
|
|
|
890
|
|
|
class ActionExecutionReadCommand(resource.ResourceCommand): |
891
|
|
|
""" |
892
|
|
|
Base class for read / view commands (list and get). |
893
|
|
|
""" |
894
|
|
|
|
895
|
|
|
def _get_exclude_attributes(self, args): |
896
|
|
|
""" |
897
|
|
|
Retrieve a list of exclude attributes for particular command line arguments. |
898
|
|
|
""" |
899
|
|
|
exclude_attributes = [] |
900
|
|
|
|
901
|
|
|
if 'result' not in args.attr: |
902
|
|
|
exclude_attributes.append('result') |
903
|
|
|
if 'trigger_instance' not in args.attr: |
904
|
|
|
exclude_attributes.append('trigger_instance') |
905
|
|
|
|
906
|
|
|
return exclude_attributes |
907
|
|
|
|
908
|
|
|
|
909
|
|
|
class ActionExecutionListCommand(ActionExecutionReadCommand): |
910
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp', |
911
|
|
|
'end_timestamp'] |
912
|
|
|
attribute_transform_functions = { |
913
|
|
|
'start_timestamp': format_isodate_for_user_timezone, |
914
|
|
|
'end_timestamp': format_isodate_for_user_timezone, |
915
|
|
|
'parameters': format_parameters, |
916
|
|
|
'status': format_status |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
920
|
|
|
super(ActionExecutionListCommand, self).__init__( |
921
|
|
|
resource, 'list', 'Get the list of the 50 most recent %s.' % |
922
|
|
|
resource.get_plural_display_name().lower(), |
923
|
|
|
*args, **kwargs) |
924
|
|
|
|
925
|
|
|
self.group = self.parser.add_argument_group() |
926
|
|
|
self.parser.add_argument('-n', '--last', type=int, dest='last', |
927
|
|
|
default=50, |
928
|
|
|
help=('List N most recent %s.' % |
929
|
|
|
resource.get_plural_display_name().lower())) |
930
|
|
|
self.parser.add_argument('-s', '--sort', type=str, dest='sort_order', |
931
|
|
|
default='descending', |
932
|
|
|
help=('Sort %s by start timestamp, ' |
933
|
|
|
'asc|ascending (earliest first) ' |
934
|
|
|
'or desc|descending (latest first)' % |
935
|
|
|
resource.get_plural_display_name().lower())) |
936
|
|
|
|
937
|
|
|
# Filter options |
938
|
|
|
self.group.add_argument('--action', help='Action reference to filter the list.') |
939
|
|
|
self.group.add_argument('--status', help=('Only return executions with the provided status.' |
940
|
|
|
' Possible values are \'%s\', \'%s\', \'%s\',' |
941
|
|
|
'\'%s\' or \'%s\'' |
942
|
|
|
'.' % POSSIBLE_ACTION_STATUS_VALUES)) |
943
|
|
|
self.group.add_argument('--trigger_instance', |
944
|
|
|
help='Trigger instance id to filter the list.') |
945
|
|
|
self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt', |
946
|
|
|
default=None, |
947
|
|
|
help=('Only return executions with timestamp ' |
948
|
|
|
'greater than the one provided. ' |
949
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
950
|
|
|
self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt', |
951
|
|
|
default=None, |
952
|
|
|
help=('Only return executions with timestamp ' |
953
|
|
|
'lower than the one provided. ' |
954
|
|
|
'Use time in the format "2000-01-01T12:00:00.000Z".')) |
955
|
|
|
self.parser.add_argument('-l', '--showall', action='store_true', |
956
|
|
|
help='') |
957
|
|
|
|
958
|
|
|
# Display options |
959
|
|
|
self.parser.add_argument('-a', '--attr', nargs='+', |
960
|
|
|
default=self.display_attributes, |
961
|
|
|
help=('List of attributes to include in the ' |
962
|
|
|
'output. "all" will return all ' |
963
|
|
|
'attributes.')) |
964
|
|
|
self.parser.add_argument('-w', '--width', nargs='+', type=int, |
965
|
|
|
default=None, |
966
|
|
|
help=('Set the width of columns in output.')) |
967
|
|
|
|
968
|
|
|
@add_auth_token_to_kwargs_from_cli |
969
|
|
|
def run(self, args, **kwargs): |
970
|
|
|
# Filtering options |
971
|
|
|
if args.action: |
972
|
|
|
kwargs['action'] = args.action |
973
|
|
|
if args.status: |
974
|
|
|
kwargs['status'] = args.status |
975
|
|
|
if args.trigger_instance: |
976
|
|
|
kwargs['trigger_instance'] = args.trigger_instance |
977
|
|
|
if not args.showall: |
978
|
|
|
# null is the magic string that translates to does not exist. |
979
|
|
|
kwargs['parent'] = 'null' |
980
|
|
|
if args.timestamp_gt: |
981
|
|
|
kwargs['timestamp_gt'] = args.timestamp_gt |
982
|
|
|
if args.timestamp_lt: |
983
|
|
|
kwargs['timestamp_lt'] = args.timestamp_lt |
984
|
|
|
if args.sort_order: |
985
|
|
|
if args.sort_order in ['asc', 'ascending']: |
986
|
|
|
kwargs['sort_asc'] = True |
987
|
|
|
elif args.sort_order in ['desc', 'descending']: |
988
|
|
|
kwargs['sort_desc'] = True |
989
|
|
|
|
990
|
|
|
# We exclude "result" and "trigger_instance" attributes which can contain a lot of data |
991
|
|
|
# since they are not displayed nor used which speeds the common operation substantially. |
992
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
993
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
994
|
|
|
kwargs['exclude_attributes'] = exclude_attributes |
995
|
|
|
|
996
|
|
|
return self.manager.query(limit=args.last, **kwargs) |
997
|
|
|
|
998
|
|
|
def run_and_print(self, args, **kwargs): |
999
|
|
|
instances = format_wf_instances(self.run(args, **kwargs)) |
1000
|
|
|
|
1001
|
|
|
if not args.json and not args.yaml: |
1002
|
|
|
# Include elapsed time for running executions |
1003
|
|
|
instances = format_execution_statuses(instances) |
1004
|
|
|
|
1005
|
|
|
self.print_output(reversed(instances), table.MultiColumnTable, |
1006
|
|
|
attributes=args.attr, widths=args.width, |
1007
|
|
|
json=args.json, |
1008
|
|
|
yaml=args.yaml, |
1009
|
|
|
attribute_transform_functions=self.attribute_transform_functions) |
1010
|
|
|
|
1011
|
|
|
|
1012
|
|
|
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand): |
1013
|
|
|
display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status', |
1014
|
|
|
'start_timestamp', 'end_timestamp', 'result', 'liveaction'] |
1015
|
|
|
|
1016
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1017
|
|
|
super(ActionExecutionGetCommand, self).__init__( |
1018
|
|
|
resource, 'get', |
1019
|
|
|
'Get individual %s.' % resource.get_display_name().lower(), |
1020
|
|
|
*args, **kwargs) |
1021
|
|
|
|
1022
|
|
|
self.parser.add_argument('id', |
1023
|
|
|
help=('ID of the %s.' % |
1024
|
|
|
resource.get_display_name().lower())) |
1025
|
|
|
|
1026
|
|
|
self._add_common_options() |
1027
|
|
|
|
1028
|
|
|
@add_auth_token_to_kwargs_from_cli |
1029
|
|
|
def run(self, args, **kwargs): |
1030
|
|
|
# We exclude "result" and / or "trigger_instance" attribute if it's not explicitly |
1031
|
|
|
# requested by user either via "--attr" flag or by default. |
1032
|
|
|
exclude_attributes = self._get_exclude_attributes(args=args) |
1033
|
|
|
exclude_attributes = ','.join(exclude_attributes) |
1034
|
|
|
|
1035
|
|
|
kwargs['params'] = {'exclude_attributes': exclude_attributes} |
1036
|
|
|
|
1037
|
|
|
execution = self.get_resource_by_id(id=args.id, **kwargs) |
1038
|
|
|
return execution |
1039
|
|
|
|
1040
|
|
|
@add_auth_token_to_kwargs_from_cli |
1041
|
|
|
def run_and_print(self, args, **kwargs): |
1042
|
|
|
try: |
1043
|
|
|
execution = self.run(args, **kwargs) |
1044
|
|
|
|
1045
|
|
|
if not args.json and not args.yaml: |
1046
|
|
|
# Include elapsed time for running executions |
1047
|
|
|
execution = format_execution_status(execution) |
1048
|
|
|
except resource.ResourceNotFoundError: |
1049
|
|
|
self.print_not_found(args.id) |
1050
|
|
|
raise OperationFailureException('Execution %s not found.' % (args.id)) |
1051
|
|
|
return self._print_execution_details(execution=execution, args=args, **kwargs) |
1052
|
|
|
|
1053
|
|
|
|
1054
|
|
|
class ActionExecutionCancelCommand(resource.ResourceCommand): |
1055
|
|
|
|
1056
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1057
|
|
|
super(ActionExecutionCancelCommand, self).__init__( |
1058
|
|
|
resource, 'cancel', 'Cancel an %s.' % |
1059
|
|
|
resource.get_plural_display_name().lower(), |
1060
|
|
|
*args, **kwargs) |
1061
|
|
|
|
1062
|
|
|
self.parser.add_argument('id', |
1063
|
|
|
help=('ID of the %s.' % |
1064
|
|
|
resource.get_display_name().lower())) |
1065
|
|
|
|
1066
|
|
|
def run(self, args, **kwargs): |
1067
|
|
|
return self.manager.delete_by_id(args.id) |
1068
|
|
|
|
1069
|
|
|
@add_auth_token_to_kwargs_from_cli |
1070
|
|
|
def run_and_print(self, args, **kwargs): |
1071
|
|
|
response = self.run(args, **kwargs) |
1072
|
|
|
if response and 'faultstring' in response: |
1073
|
|
|
message = response.get('faultstring', 'Cancellation requested for %s with id %s.' % |
1074
|
|
|
(self.resource.get_display_name().lower(), args.id)) |
1075
|
|
|
|
1076
|
|
|
elif response: |
1077
|
|
|
message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(), |
1078
|
|
|
args.id) |
1079
|
|
|
else: |
1080
|
|
|
message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(), |
1081
|
|
|
args.id) |
1082
|
|
|
print(message) |
1083
|
|
|
|
1084
|
|
|
|
1085
|
|
|
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand): |
1086
|
|
|
def __init__(self, resource, *args, **kwargs): |
|
|
|
|
1087
|
|
|
|
1088
|
|
|
super(ActionExecutionReRunCommand, self).__init__( |
1089
|
|
|
resource, kwargs.pop('name', 're-run'), |
1090
|
|
|
'A command to re-run a particular action.', |
1091
|
|
|
*args, **kwargs) |
1092
|
|
|
|
1093
|
|
|
self.parser.add_argument('id', nargs='?', |
1094
|
|
|
metavar='id', |
1095
|
|
|
help='ID of action execution to re-run ') |
1096
|
|
|
self.parser.add_argument('parameters', nargs='*', |
1097
|
|
|
help='List of keyword args, positional args, ' |
1098
|
|
|
'and optional args for the action.') |
1099
|
|
|
self.parser.add_argument('--tasks', nargs='*', |
1100
|
|
|
help='Name of the workflow tasks to re-run.') |
1101
|
|
|
self.parser.add_argument('--no-reset', dest='no_reset', nargs='*', |
1102
|
|
|
help='Name of the with-items tasks to not reset. This only ' |
1103
|
|
|
'applies to Mistral workflows. By default, all iterations ' |
1104
|
|
|
'for with-items tasks is rerun. If no reset, only failed ' |
1105
|
|
|
' iterations are rerun.') |
1106
|
|
|
self.parser.add_argument('-a', '--async', |
1107
|
|
|
action='store_true', dest='async', |
1108
|
|
|
help='Do not wait for action to finish.') |
1109
|
|
|
self.parser.add_argument('-e', '--inherit-env', |
1110
|
|
|
action='store_true', dest='inherit_env', |
1111
|
|
|
help='Pass all the environment variables ' |
1112
|
|
|
'which are accessible to the CLI as "env" ' |
1113
|
|
|
'parameter to the action. Note: Only works ' |
1114
|
|
|
'with python, local and remote runners.') |
1115
|
|
|
self.parser.add_argument('-h', '--help', |
1116
|
|
|
action='store_true', dest='help', |
1117
|
|
|
help='Print usage for the given action.') |
1118
|
|
|
|
1119
|
|
|
self._add_common_options() |
1120
|
|
|
|
1121
|
|
|
@add_auth_token_to_kwargs_from_cli |
1122
|
|
|
def run(self, args, **kwargs): |
1123
|
|
|
existing_execution = self.manager.get_by_id(args.id, **kwargs) |
1124
|
|
|
|
1125
|
|
|
if not existing_execution: |
1126
|
|
|
raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' % |
1127
|
|
|
(args.id)) |
1128
|
|
|
|
1129
|
|
|
action_mgr = self.app.client.managers['Action'] |
1130
|
|
|
runner_mgr = self.app.client.managers['RunnerType'] |
1131
|
|
|
action_exec_mgr = self.app.client.managers['LiveAction'] |
1132
|
|
|
|
1133
|
|
|
action_ref = existing_execution.action['ref'] |
1134
|
|
|
action = action_mgr.get_by_ref_or_id(action_ref) |
1135
|
|
|
runner = runner_mgr.get_by_name(action.runner_type) |
1136
|
|
|
|
1137
|
|
|
action_parameters = self._get_action_parameters_from_args(action=action, runner=runner, |
1138
|
|
|
args=args) |
1139
|
|
|
|
1140
|
|
|
execution = action_exec_mgr.re_run(execution_id=args.id, |
1141
|
|
|
parameters=action_parameters, |
1142
|
|
|
tasks=args.tasks, |
1143
|
|
|
no_reset=args.no_reset, |
1144
|
|
|
**kwargs) |
1145
|
|
|
|
1146
|
|
|
execution = self._get_execution_result(execution=execution, |
1147
|
|
|
action_exec_mgr=action_exec_mgr, |
1148
|
|
|
args=args, **kwargs) |
1149
|
|
|
|
1150
|
|
|
return execution |
1151
|
|
|
|
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: