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 sys |
||
17 | |||
18 | import editor |
||
19 | import yaml |
||
20 | |||
21 | from st2client.models import Config |
||
22 | from st2client.models import Pack |
||
23 | from st2client.models import LiveAction |
||
24 | from st2client.commands import resource |
||
25 | from st2client.commands.resource import add_auth_token_to_kwargs_from_cli |
||
26 | from st2client.commands.action import ActionRunCommandMixin |
||
27 | from st2client.formatters import table |
||
28 | from st2client.exceptions.operations import OperationFailureException |
||
29 | import st2client.utils.terminal as term |
||
30 | from st2client.utils import interactive |
||
31 | |||
32 | |||
33 | LIVEACTION_STATUS_REQUESTED = 'requested' |
||
34 | LIVEACTION_STATUS_SCHEDULED = 'scheduled' |
||
35 | LIVEACTION_STATUS_DELAYED = 'delayed' |
||
36 | LIVEACTION_STATUS_RUNNING = 'running' |
||
37 | LIVEACTION_STATUS_SUCCEEDED = 'succeeded' |
||
38 | LIVEACTION_STATUS_FAILED = 'failed' |
||
39 | LIVEACTION_STATUS_TIMED_OUT = 'timeout' |
||
40 | LIVEACTION_STATUS_ABANDONED = 'abandoned' |
||
41 | LIVEACTION_STATUS_CANCELING = 'canceling' |
||
42 | LIVEACTION_STATUS_CANCELED = 'canceled' |
||
43 | |||
44 | LIVEACTION_COMPLETED_STATES = [ |
||
45 | LIVEACTION_STATUS_SUCCEEDED, |
||
46 | LIVEACTION_STATUS_FAILED, |
||
47 | LIVEACTION_STATUS_TIMED_OUT, |
||
48 | LIVEACTION_STATUS_CANCELED, |
||
49 | LIVEACTION_STATUS_ABANDONED |
||
50 | ] |
||
51 | |||
52 | |||
53 | class PackBranch(resource.ResourceBranch): |
||
54 | def __init__(self, description, app, subparsers, parent_parser=None): |
||
55 | super(PackBranch, self).__init__( |
||
56 | Pack, description, app, subparsers, |
||
57 | parent_parser=parent_parser, |
||
58 | read_only=True, |
||
59 | commands={ |
||
60 | 'list': PackListCommand, |
||
61 | 'get': PackGetCommand |
||
62 | }) |
||
63 | |||
64 | self.commands['show'] = PackShowCommand(self.resource, self.app, self.subparsers) |
||
65 | self.commands['search'] = PackSearchCommand(self.resource, self.app, self.subparsers) |
||
66 | self.commands['install'] = PackInstallCommand(self.resource, self.app, self.subparsers) |
||
67 | self.commands['remove'] = PackRemoveCommand(self.resource, self.app, self.subparsers) |
||
68 | self.commands['register'] = PackRegisterCommand(self.resource, self.app, self.subparsers) |
||
69 | self.commands['config'] = PackConfigCommand(self.resource, self.app, self.subparsers) |
||
70 | |||
71 | |||
72 | class PackResourceCommand(resource.ResourceCommand): |
||
73 | def run_and_print(self, args, **kwargs): |
||
74 | try: |
||
75 | instance = self.run(args, **kwargs) |
||
76 | if not instance: |
||
77 | raise resource.ResourceNotFoundError("No matching items found") |
||
78 | self.print_output(instance, table.PropertyValueTable, |
||
79 | attributes=['all'], json=args.json, yaml=args.yaml) |
||
80 | except resource.ResourceNotFoundError: |
||
81 | print("No matching items found") |
||
82 | except Exception as e: |
||
83 | message = e.message or str(e) |
||
84 | print('ERROR: %s' % (message)) |
||
85 | raise OperationFailureException(message) |
||
86 | |||
87 | |||
88 | class PackAsyncCommand(ActionRunCommandMixin, resource.ResourceCommand): |
||
89 | def __init__(self, *args, **kwargs): |
||
90 | super(PackAsyncCommand, self).__init__(*args, **kwargs) |
||
91 | |||
92 | self.parser.add_argument('-w', '--width', nargs='+', type=int, default=None, |
||
93 | help='Set the width of columns in output.') |
||
94 | |||
95 | detail_arg_grp = self.parser.add_mutually_exclusive_group() |
||
96 | detail_arg_grp.add_argument('--attr', nargs='+', |
||
97 | default=['name', 'description', 'version', 'author'], |
||
98 | help=('List of attributes to include in the ' |
||
99 | 'output. "all" or unspecified will ' |
||
100 | 'return all attributes.')) |
||
101 | detail_arg_grp.add_argument('-d', '--detail', action='store_true', |
||
102 | help='Display full detail of the execution in table format.') |
||
103 | |||
104 | @add_auth_token_to_kwargs_from_cli |
||
105 | def run_and_print(self, args, **kwargs): |
||
106 | instance = self.run(args, **kwargs) |
||
107 | if not instance: |
||
108 | raise Exception('Server did not create instance.') |
||
109 | |||
110 | parent_id = instance.execution_id |
||
111 | |||
112 | stream_mgr = self.app.client.managers['Stream'] |
||
113 | |||
114 | execution = None |
||
115 | |||
116 | with term.TaskIndicator() as indicator: |
||
117 | events = ['st2.execution__create', 'st2.execution__update'] |
||
118 | for event in stream_mgr.listen(events, **kwargs): |
||
119 | execution = LiveAction(**event) |
||
120 | |||
121 | if execution.id == parent_id \ |
||
122 | and execution.status in LIVEACTION_COMPLETED_STATES: |
||
123 | break |
||
124 | |||
125 | # Suppress intermediate output in case output formatter is requested |
||
126 | if args.json or args.yaml: |
||
127 | continue |
||
128 | |||
129 | if getattr(execution, 'parent', None) == parent_id: |
||
130 | status = execution.status |
||
131 | name = execution.context['chain']['name'] |
||
132 | |||
133 | if status == LIVEACTION_STATUS_SCHEDULED: |
||
134 | indicator.add_stage(status, name) |
||
135 | if status == LIVEACTION_STATUS_RUNNING: |
||
136 | indicator.update_stage(status, name) |
||
137 | if status in LIVEACTION_COMPLETED_STATES: |
||
138 | indicator.finish_stage(status, name) |
||
139 | |||
140 | if execution and execution.status == LIVEACTION_STATUS_FAILED: |
||
141 | args.depth = 1 |
||
142 | self._print_execution_details(execution=execution, args=args, **kwargs) |
||
143 | sys.exit(1) |
||
144 | |||
145 | return self.app.client.managers['LiveAction'].get_by_id(parent_id, **kwargs) |
||
146 | |||
147 | |||
148 | class PackListCommand(resource.ResourceListCommand): |
||
149 | display_attributes = ['ref', 'name', 'description', 'version', 'author'] |
||
150 | attribute_display_order = ['ref', 'name', 'description', 'version', 'author'] |
||
151 | |||
152 | |||
153 | class PackGetCommand(resource.ResourceGetCommand): |
||
154 | pk_argument_name = 'ref' |
||
155 | display_attributes = ['name', 'version', 'author', 'email', 'keywords', 'description'] |
||
156 | attribute_display_order = ['name', 'version', 'author', 'email', 'keywords', 'description'] |
||
157 | help_string = 'Get information about an installed pack.' |
||
158 | |||
159 | |||
160 | class PackShowCommand(PackResourceCommand): |
||
161 | def __init__(self, resource, *args, **kwargs): |
||
0 ignored issues
–
show
|
|||
162 | help_string = ('Get information about an available %s from the index.' % |
||
163 | resource.get_display_name().lower()) |
||
164 | super(PackShowCommand, self).__init__(resource, 'show', help_string, |
||
165 | *args, **kwargs) |
||
166 | |||
167 | self.parser.add_argument('pack', |
||
168 | help='Name of the %s to show.' % |
||
169 | resource.get_display_name().lower()) |
||
170 | |||
171 | @add_auth_token_to_kwargs_from_cli |
||
172 | def run(self, args, **kwargs): |
||
173 | return self.manager.search(args, **kwargs) |
||
174 | |||
175 | |||
176 | class PackInstallCommand(PackAsyncCommand): |
||
177 | 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 24 ).
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
![]() |
|||
178 | super(PackInstallCommand, self).__init__(resource, 'install', 'Install new %s.' |
||
179 | % resource.get_plural_display_name().lower(), |
||
180 | *args, **kwargs) |
||
181 | |||
182 | self.parser.add_argument('packs', |
||
183 | nargs='+', |
||
184 | metavar='pack', |
||
185 | help='Name of the %s in Exchange, or a git repo URL.' % |
||
186 | resource.get_plural_display_name().lower()) |
||
187 | self.parser.add_argument('--force', |
||
188 | action='store_true', |
||
189 | default=False, |
||
190 | help='Force pack installation.') |
||
191 | |||
192 | def run(self, args, **kwargs): |
||
193 | self._get_content_counts_for_pack(args, **kwargs) |
||
194 | return self.manager.install(args.packs, force=args.force, **kwargs) |
||
195 | |||
196 | def _get_content_counts_for_pack(self, args, **kwargs): |
||
197 | # Global content list, excluding "tests" |
||
198 | pack_content = {'actions': 0, 'rules': 0, 'sensors': 0, 'aliases': 0, 'triggers': 0} |
||
199 | |||
200 | if len(args.packs) == 1: |
||
201 | args.pack = args.packs[0] |
||
202 | pack_info = self.manager.search(args, **kwargs) |
||
203 | content = getattr(pack_info, 'content', {}) |
||
204 | |||
205 | if content: |
||
206 | for entity in content.keys(): |
||
207 | if entity in pack_content: |
||
208 | pack_content[entity] += content[entity]['count'] |
||
209 | self._print_pack_content(args.packs, pack_content) |
||
210 | |||
211 | else: |
||
212 | pack_content = pack_content.fromkeys(pack_content, 0) |
||
213 | # TODO: Better solution is to update endpoint query param for one API call |
||
214 | # example: ?packs=pack1,pack2,pack3 |
||
215 | for pack in args.packs: |
||
216 | # args.pack required for search |
||
217 | args.pack = pack |
||
218 | pack_info = self.manager.search(args, **kwargs) |
||
219 | content = getattr(pack_info, 'content', {}) |
||
220 | |||
221 | if content: |
||
222 | for entity in content.keys(): |
||
223 | if entity in pack_content: |
||
224 | pack_content[entity] += content[entity]['count'] |
||
225 | if content: |
||
226 | self._print_pack_content(args.packs, pack_content) |
||
227 | |||
228 | @staticmethod |
||
229 | def _print_pack_content(pack_name, pack_content): |
||
230 | print('\nFor the "%s" %s, the following content will be registered:\n' |
||
231 | % (', '.join(pack_name), 'pack' if len(pack_name) == 1 else 'packs')) |
||
232 | for item, count in pack_content.items(): |
||
233 | print('%-10s| %s' % (item, count)) |
||
234 | print('\nInstallation may take a while for packs with many items.') |
||
235 | |||
236 | @add_auth_token_to_kwargs_from_cli |
||
237 | def run_and_print(self, args, **kwargs): |
||
238 | instance = super(PackInstallCommand, self).run_and_print(args, **kwargs) |
||
239 | # Hack to get a list of resolved references of installed packs |
||
240 | packs = instance.result['tasks'][1]['result']['result'] |
||
241 | |||
242 | if len(packs) == 1: |
||
243 | pack_instance = self.app.client.managers['Pack'].get_by_ref_or_id(packs[0], **kwargs) |
||
244 | self.print_output(pack_instance, table.PropertyValueTable, |
||
245 | attributes=args.attr, json=args.json, yaml=args.yaml, |
||
246 | attribute_display_order=self.attribute_display_order) |
||
247 | else: |
||
248 | all_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs) |
||
249 | pack_instances = [] |
||
250 | |||
251 | for pack in all_pack_instances: |
||
252 | if pack.name in packs: |
||
253 | pack_instances.append(pack) |
||
254 | |||
255 | self.print_output(pack_instances, table.MultiColumnTable, |
||
256 | attributes=args.attr, widths=args.width, |
||
257 | json=args.json, yaml=args.yaml) |
||
258 | |||
259 | |||
260 | class PackRemoveCommand(PackAsyncCommand): |
||
261 | 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 24 ).
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
![]() |
|||
262 | super(PackRemoveCommand, self).__init__(resource, 'remove', 'Remove %s.' |
||
263 | % resource.get_plural_display_name().lower(), |
||
264 | *args, **kwargs) |
||
265 | |||
266 | self.parser.add_argument('packs', |
||
267 | nargs='+', |
||
268 | metavar='pack', |
||
269 | help='Name of the %s to remove.' % |
||
270 | resource.get_plural_display_name().lower()) |
||
271 | |||
272 | def run(self, args, **kwargs): |
||
273 | return self.manager.remove(args.packs, **kwargs) |
||
274 | |||
275 | @add_auth_token_to_kwargs_from_cli |
||
276 | def run_and_print(self, args, **kwargs): |
||
277 | all_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs) |
||
278 | |||
279 | super(PackRemoveCommand, self).run_and_print(args, **kwargs) |
||
280 | |||
281 | packs = args.packs |
||
282 | |||
283 | if len(packs) == 1: |
||
284 | pack_instance = self.app.client.managers['Pack'].get_by_ref_or_id(packs[0], **kwargs) |
||
285 | |||
286 | if pack_instance: |
||
287 | raise OperationFailureException('Pack %s has not been removed properly', packs[0]) |
||
288 | |||
289 | removed_pack_instance = next((pack for pack in all_pack_instances |
||
290 | if pack.name == packs[0]), None) |
||
291 | |||
292 | self.print_output(removed_pack_instance, table.PropertyValueTable, |
||
293 | attributes=args.attr, json=args.json, yaml=args.yaml, |
||
294 | attribute_display_order=self.attribute_display_order) |
||
295 | else: |
||
296 | remaining_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs) |
||
297 | pack_instances = [] |
||
298 | |||
299 | for pack in all_pack_instances: |
||
300 | if pack.name in packs: |
||
301 | pack_instances.append(pack) |
||
302 | if pack in remaining_pack_instances: |
||
303 | raise OperationFailureException('Pack %s has not been removed properly', |
||
304 | pack.name) |
||
305 | |||
306 | self.print_output(pack_instances, table.MultiColumnTable, |
||
307 | attributes=args.attr, widths=args.width, |
||
308 | json=args.json, yaml=args.yaml) |
||
309 | |||
310 | |||
311 | class PackRegisterCommand(PackResourceCommand): |
||
312 | 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 24 ).
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
![]() |
|||
313 | super(PackRegisterCommand, self).__init__(resource, 'register', |
||
314 | 'Register %s(s): sync all file changes with DB.' |
||
315 | % resource.get_display_name().lower(), |
||
316 | *args, **kwargs) |
||
317 | |||
318 | self.parser.add_argument('packs', |
||
319 | nargs='*', |
||
320 | metavar='pack', |
||
321 | help='Name of the %s(s) to register.' % |
||
322 | resource.get_display_name().lower()) |
||
323 | |||
324 | self.parser.add_argument('--types', |
||
325 | nargs='+', |
||
326 | help='Types of content to register.') |
||
327 | |||
328 | @add_auth_token_to_kwargs_from_cli |
||
329 | def run(self, args, **kwargs): |
||
330 | return self.manager.register(args.packs, args.types, **kwargs) |
||
331 | |||
332 | |||
333 | class PackSearchCommand(resource.ResourceTableCommand): |
||
334 | display_attributes = ['name', 'description', 'version', 'author'] |
||
335 | attribute_display_order = ['name', 'description', 'version', 'author'] |
||
336 | |||
337 | 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 24 ).
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
![]() |
|||
338 | super(PackSearchCommand, self).__init__(resource, 'search', |
||
339 | 'Search the index for a %s with any attribute \ |
||
340 | matching the query.' |
||
341 | % resource.get_display_name().lower(), |
||
342 | *args, **kwargs) |
||
343 | |||
344 | self.parser.add_argument('query', |
||
345 | help='Search query.') |
||
346 | |||
347 | @add_auth_token_to_kwargs_from_cli |
||
348 | def run(self, args, **kwargs): |
||
349 | return self.manager.search(args, **kwargs) |
||
350 | |||
351 | |||
352 | class PackConfigCommand(resource.ResourceCommand): |
||
353 | 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 24 ).
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
![]() |
|||
354 | super(PackConfigCommand, self).__init__(resource, 'config', |
||
355 | 'Configure a %s based on config schema.' |
||
356 | % resource.get_display_name().lower(), |
||
357 | *args, **kwargs) |
||
358 | |||
359 | self.parser.add_argument('name', |
||
360 | help='Name of the %s(s) to configure.' % |
||
361 | resource.get_display_name().lower()) |
||
362 | |||
363 | @add_auth_token_to_kwargs_from_cli |
||
364 | def run(self, args, **kwargs): |
||
365 | schema = self.app.client.managers['ConfigSchema'].get_by_ref_or_id(args.name, **kwargs) |
||
366 | |||
367 | if not schema: |
||
368 | msg = '%s "%s" doesn\'t exist or doesn\'t have a config schema defined.' |
||
369 | raise resource.ResourceNotFoundError(msg % (self.resource.get_display_name(), |
||
370 | args.name)) |
||
371 | |||
372 | config = interactive.InteractiveForm(schema.attributes).initiate_dialog() |
||
373 | |||
374 | message = '---\nDo you want to preview the config in an editor before saving?' |
||
375 | description = 'Secrets will be shown in plain text.' |
||
376 | preview_dialog = interactive.Question(message, {'default': 'y', |
||
377 | 'description': description}) |
||
378 | if preview_dialog.read() == 'y': |
||
379 | try: |
||
380 | contents = yaml.safe_dump(config, indent=4, default_flow_style=False) |
||
381 | modified = editor.edit(contents=contents) |
||
382 | config = yaml.safe_load(modified) |
||
383 | except editor.EditorError as e: |
||
384 | print(str(e)) |
||
385 | |||
386 | message = '---\nDo you want me to save it?' |
||
387 | save_dialog = interactive.Question(message, {'default': 'y'}) |
||
388 | if save_dialog.read() == 'n': |
||
389 | raise OperationFailureException('Interrupted') |
||
390 | |||
391 | config_item = Config(pack=args.name, values=config) |
||
392 | result = self.app.client.managers['Config'].update(config_item, **kwargs) |
||
393 | |||
394 | return result |
||
395 | |||
396 | def run_and_print(self, args, **kwargs): |
||
397 | try: |
||
398 | instance = self.run(args, **kwargs) |
||
399 | if not instance: |
||
400 | raise Exception("Configuration failed") |
||
401 | self.print_output(instance, table.PropertyValueTable, |
||
402 | attributes=['all'], json=args.json, yaml=args.yaml) |
||
403 | except (KeyboardInterrupt, SystemExit): |
||
404 | raise OperationFailureException('Interrupted') |
||
405 | except Exception as e: |
||
406 | if self.app.client.debug: |
||
407 | raise |
||
408 | |||
409 | message = e.message or str(e) |
||
410 | print('ERROR: %s' % (message)) |
||
411 | raise OperationFailureException(message) |
||
412 |
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: