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