Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2client/st2client/commands/pack.py (5 issues)

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
import sys
18
19
import editor
20
import yaml
21
22
from st2client.models import Config
23
from st2client.models import Pack
24
from st2client.models import LiveAction
25
from st2client.commands import resource
26
from st2client.commands.resource import add_auth_token_to_kwargs_from_cli
27
from st2client.commands.action import ActionRunCommandMixin
28
from st2client.formatters import table
29
from st2client.exceptions.operations import OperationFailureException
30
import st2client.utils.terminal as term
31
from st2client.utils import interactive
32
33
34
LIVEACTION_STATUS_REQUESTED = 'requested'
35
LIVEACTION_STATUS_SCHEDULED = 'scheduled'
36
LIVEACTION_STATUS_DELAYED = 'delayed'
37
LIVEACTION_STATUS_RUNNING = 'running'
38
LIVEACTION_STATUS_SUCCEEDED = 'succeeded'
39
LIVEACTION_STATUS_FAILED = 'failed'
40
LIVEACTION_STATUS_TIMED_OUT = 'timeout'
41
LIVEACTION_STATUS_ABANDONED = 'abandoned'
42
LIVEACTION_STATUS_CANCELING = 'canceling'
43
LIVEACTION_STATUS_CANCELED = 'canceled'
44
45
LIVEACTION_COMPLETED_STATES = [
46
    LIVEACTION_STATUS_SUCCEEDED,
47
    LIVEACTION_STATUS_FAILED,
48
    LIVEACTION_STATUS_TIMED_OUT,
49
    LIVEACTION_STATUS_CANCELED,
50
    LIVEACTION_STATUS_ABANDONED
51
]
52
53
54
class PackBranch(resource.ResourceBranch):
55
    def __init__(self, description, app, subparsers, parent_parser=None):
56
        super(PackBranch, self).__init__(
57
            Pack, description, app, subparsers,
58
            parent_parser=parent_parser,
59
            read_only=True,
60
            commands={
61
                'list': PackListCommand,
62
                'get': PackGetCommand
63
            })
64
65
        self.commands['show'] = PackShowCommand(self.resource, self.app, self.subparsers)
66
        self.commands['search'] = PackSearchCommand(self.resource, self.app, self.subparsers)
67
        self.commands['install'] = PackInstallCommand(self.resource, self.app, self.subparsers)
68
        self.commands['remove'] = PackRemoveCommand(self.resource, self.app, self.subparsers)
69
        self.commands['register'] = PackRegisterCommand(self.resource, self.app, self.subparsers)
70
        self.commands['config'] = PackConfigCommand(self.resource, self.app, self.subparsers)
71
72
73
class PackResourceCommand(resource.ResourceCommand):
74
    def run_and_print(self, args, **kwargs):
75
        try:
76
            instance = self.run(args, **kwargs)
77
            if not instance:
78
                raise resource.ResourceNotFoundError("No matching items found")
79
            self.print_output(instance, table.PropertyValueTable,
80
                              attributes=['all'], json=args.json, yaml=args.yaml)
81
        except resource.ResourceNotFoundError:
82
            print("No matching items found")
83
        except Exception as e:
84
            message = e.message or str(e)
85
            print('ERROR: %s' % (message))
86
            raise OperationFailureException(message)
87
88
89
class PackAsyncCommand(ActionRunCommandMixin, resource.ResourceCommand):
90
    def __init__(self, *args, **kwargs):
91
        super(PackAsyncCommand, self).__init__(*args, **kwargs)
92
93
        self.parser.add_argument('-w', '--width', nargs='+', type=int, default=None,
94
                                       help='Set the width of columns in output.')
95
96
        detail_arg_grp = self.parser.add_mutually_exclusive_group()
97
        detail_arg_grp.add_argument('--attr', nargs='+',
98
                                    default=['name', 'description', 'version', 'author'],
99
                                    help=('List of attributes to include in the '
100
                                          'output. "all" or unspecified will '
101
                                          'return all attributes.'))
102
        detail_arg_grp.add_argument('-d', '--detail', action='store_true',
103
                                    help='Display full detail of the execution in table format.')
104
105
    @add_auth_token_to_kwargs_from_cli
106
    def run_and_print(self, args, **kwargs):
107
        instance = self.run(args, **kwargs)
108
        if not instance:
109
            raise Exception('Server did not create instance.')
110
111
        parent_id = instance.execution_id
112
113
        stream_mgr = self.app.client.managers['Stream']
114
115
        execution = None
116
117
        with term.TaskIndicator() as indicator:
118
            events = ['st2.execution__create', 'st2.execution__update']
119
            for event in stream_mgr.listen(events, **kwargs):
120
                execution = LiveAction(**event)
121
122
                if execution.id == parent_id \
123
                        and execution.status in LIVEACTION_COMPLETED_STATES:
124
                    break
125
126
                # Suppress intermediate output in case output formatter is requested
127
                if args.json or args.yaml:
128
                    continue
129
130
                if getattr(execution, 'parent', None) == parent_id:
131
                    status = execution.status
132
                    name = execution.context['chain']['name']
133
134
                    if status == LIVEACTION_STATUS_SCHEDULED:
135
                        indicator.add_stage(status, name)
136
                    if status == LIVEACTION_STATUS_RUNNING:
137
                        indicator.update_stage(status, name)
138
                    if status in LIVEACTION_COMPLETED_STATES:
139
                        indicator.finish_stage(status, name)
140
141
        if execution and execution.status == LIVEACTION_STATUS_FAILED:
142
            args.depth = 1
143
            self._print_execution_details(execution=execution, args=args, **kwargs)
144
            sys.exit(1)
145
146
        return self.app.client.managers['LiveAction'].get_by_id(parent_id, **kwargs)
147
148
149
class PackListCommand(resource.ResourceListCommand):
150
    display_attributes = ['ref', 'name', 'description', 'version', 'author']
151
    attribute_display_order = ['ref', 'name', 'description', 'version', 'author']
152
153
154
class PackGetCommand(resource.ResourceGetCommand):
155
    pk_argument_name = 'ref'
156
    display_attributes = ['name', 'version', 'author', 'email', 'keywords', 'description']
157
    attribute_display_order = ['name', 'version', 'author', 'email', 'keywords', 'description']
158
    help_string = 'Get information about an installed pack.'
159
160
161
class PackShowCommand(PackResourceCommand):
162
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 25).

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...
163
        help_string = ('Get information about an available %s from the index.' %
164
                       resource.get_display_name().lower())
165
        super(PackShowCommand, self).__init__(resource, 'show', help_string,
166
                                              *args, **kwargs)
167
168
        self.parser.add_argument('pack',
169
                                 help='Name of the %s to show.' %
170
                                 resource.get_display_name().lower())
171
172
    @add_auth_token_to_kwargs_from_cli
173
    def run(self, args, **kwargs):
174
        return self.manager.search(args, **kwargs)
175
176
177
class PackInstallCommand(PackAsyncCommand):
178
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 25).

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...
179
        super(PackInstallCommand, self).__init__(resource, 'install', 'Install new %s.'
180
                                                 % resource.get_plural_display_name().lower(),
181
                                                 *args, **kwargs)
182
183
        self.parser.add_argument('packs',
184
                                 nargs='+',
185
                                 metavar='pack',
186
                                 help='Name of the %s in Exchange, or a git repo URL.' %
187
                                 resource.get_plural_display_name().lower())
188
        self.parser.add_argument('--python3',
189
                                 action='store_true',
190
                                 default=False,
191
                                 help='Use Python 3 binary for pack virtual environment.')
192
        self.parser.add_argument('--force',
193
                                 action='store_true',
194
                                 default=False,
195
                                 help='Force pack installation.')
196
197
    def run(self, args, **kwargs):
198
        self._get_content_counts_for_pack(args, **kwargs)
199
        return self.manager.install(args.packs, python3=args.python3, force=args.force, **kwargs)
200
201
    def _get_content_counts_for_pack(self, args, **kwargs):
202
        # Global content list, excluding "tests"
203
        # Note: We skip this step for local packs
204
        pack_content = {'actions': 0, 'rules': 0, 'sensors': 0, 'aliases': 0, 'triggers': 0}
205
206
        if len(args.packs) == 1:
207
            args.pack = args.packs[0]
208
209
            if args.pack.startswith('file://'):
210
                return
211
212
            pack_info = self.manager.search(args, ignore_errors=True, **kwargs)
213
            content = getattr(pack_info, 'content', {})
214
215
            if content:
216
                for entity in content.keys():
217
                    if entity in pack_content:
218
                        pack_content[entity] += content[entity]['count']
219
                self._print_pack_content(args.packs, pack_content)
220
221
        else:
222
            pack_content = pack_content.fromkeys(pack_content, 0)
223
            # TODO: Better solution is to update endpoint query param for one API call
224
            #       example: ?packs=pack1,pack2,pack3
225
            for pack in args.packs:
226
                # args.pack required for search
227
                args.pack = pack
228
229
                if args.pack.startswith('file://'):
230
                    return
231
232
                pack_info = self.manager.search(args, ignore_errors=True, **kwargs)
233
                content = getattr(pack_info, 'content', {})
234
235
                if content:
236
                    for entity in content.keys():
237
                        if entity in pack_content:
238
                            pack_content[entity] += content[entity]['count']
239
            if content:
240
                self._print_pack_content(args.packs, pack_content)
241
242
    @staticmethod
243
    def _print_pack_content(pack_name, pack_content):
244
        print('\nFor the "%s" %s, the following content will be registered:\n'
245
              % (', '.join(pack_name), 'pack' if len(pack_name) == 1 else 'packs'))
246
        for item, count in pack_content.items():
247
            print('%-10s|  %s' % (item, count))
248
        print('\nInstallation may take a while for packs with many items.')
249
250
    @add_auth_token_to_kwargs_from_cli
251
    def run_and_print(self, args, **kwargs):
252
        instance = super(PackInstallCommand, self).run_and_print(args, **kwargs)
253
        # Hack to get a list of resolved references of installed packs
254
        packs = instance.result['tasks'][1]['result']['result']
255
256
        if len(packs) == 1:
257
            pack_instance = self.app.client.managers['Pack'].get_by_ref_or_id(packs[0], **kwargs)
258
            self.print_output(pack_instance, table.PropertyValueTable,
259
                              attributes=args.attr, json=args.json, yaml=args.yaml,
260
                              attribute_display_order=self.attribute_display_order)
261
        else:
262
            all_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs)
263
            pack_instances = []
264
265
            for pack in all_pack_instances:
266
                if pack.name in packs:
267
                    pack_instances.append(pack)
268
269
            self.print_output(pack_instances, table.MultiColumnTable,
270
                              attributes=args.attr, widths=args.width,
271
                              json=args.json, yaml=args.yaml)
272
273
274
class PackRemoveCommand(PackAsyncCommand):
275
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 25).

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...
276
        super(PackRemoveCommand, self).__init__(resource, 'remove', 'Remove %s.'
277
                                                % resource.get_plural_display_name().lower(),
278
                                                *args, **kwargs)
279
280
        self.parser.add_argument('packs',
281
                                 nargs='+',
282
                                 metavar='pack',
283
                                 help='Name of the %s to remove.' %
284
                                 resource.get_plural_display_name().lower())
285
286
    def run(self, args, **kwargs):
287
        return self.manager.remove(args.packs, **kwargs)
288
289
    @add_auth_token_to_kwargs_from_cli
290
    def run_and_print(self, args, **kwargs):
291
        all_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs)
292
293
        super(PackRemoveCommand, self).run_and_print(args, **kwargs)
294
295
        packs = args.packs
296
297
        if len(packs) == 1:
298
            pack_instance = self.app.client.managers['Pack'].get_by_ref_or_id(packs[0], **kwargs)
299
300
            if pack_instance:
301
                raise OperationFailureException('Pack %s has not been removed properly', packs[0])
302
303
            removed_pack_instance = next((pack for pack in all_pack_instances
304
                                         if pack.name == packs[0]), None)
305
306
            self.print_output(removed_pack_instance, table.PropertyValueTable,
307
                              attributes=args.attr, json=args.json, yaml=args.yaml,
308
                              attribute_display_order=self.attribute_display_order)
309
        else:
310
            remaining_pack_instances = self.app.client.managers['Pack'].get_all(**kwargs)
311
            pack_instances = []
312
313
            for pack in all_pack_instances:
314
                if pack.name in packs:
315
                    pack_instances.append(pack)
316
                if pack in remaining_pack_instances:
317
                    raise OperationFailureException('Pack %s has not been removed properly',
318
                                                    pack.name)
319
320
            self.print_output(pack_instances, table.MultiColumnTable,
321
                              attributes=args.attr, widths=args.width,
322
                              json=args.json, yaml=args.yaml)
323
324
325
class PackRegisterCommand(PackResourceCommand):
326
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 25).

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...
327
        super(PackRegisterCommand, self).__init__(resource, 'register',
328
                                                  'Register %s(s): sync all file changes with DB.'
329
                                                  % resource.get_display_name().lower(),
330
                                                  *args, **kwargs)
331
332
        self.parser.add_argument('packs',
333
                                 nargs='*',
334
                                 metavar='pack',
335
                                 help='Name of the %s(s) to register.' %
336
                                 resource.get_display_name().lower())
337
338
        self.parser.add_argument('--types',
339
                                 nargs='+',
340
                                 help='Types of content to register.')
341
342
    @add_auth_token_to_kwargs_from_cli
343
    def run(self, args, **kwargs):
344
        return self.manager.register(args.packs, args.types, **kwargs)
345
346
347
class PackSearchCommand(resource.ResourceTableCommand):
348
    display_attributes = ['name', 'description', 'version', 'author']
349
    attribute_display_order = ['name', 'description', 'version', 'author']
350
351
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 25).

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...
352
        super(PackSearchCommand, self).__init__(resource, 'search',
353
                                                'Search the index for a %s with any attribute \
354
                                                matching the query.'
355
                                                % resource.get_display_name().lower(),
356
                                                *args, **kwargs)
357
358
        self.parser.add_argument('query',
359
                                 help='Search query.')
360
361
    @add_auth_token_to_kwargs_from_cli
362
    def run(self, args, **kwargs):
363
        return self.manager.search(args, **kwargs)
364
365
366
class PackConfigCommand(resource.ResourceCommand):
367
    def __init__(self, resource, *args, **kwargs):
368
        super(PackConfigCommand, self).__init__(resource, 'config',
369
                                                'Configure a %s based on config schema.'
370
                                                % resource.get_display_name().lower(),
371
                                                *args, **kwargs)
372
373
        self.parser.add_argument('name',
374
                                 help='Name of the %s(s) to configure.' %
375
                                      resource.get_display_name().lower())
376
377
    @add_auth_token_to_kwargs_from_cli
378
    def run(self, args, **kwargs):
379
        schema = self.app.client.managers['ConfigSchema'].get_by_ref_or_id(args.name, **kwargs)
380
381
        if not schema:
382
            msg = '%s "%s" doesn\'t exist or doesn\'t have a config schema defined.'
383
            raise resource.ResourceNotFoundError(msg % (self.resource.get_display_name(),
384
                                                        args.name))
385
386
        config = interactive.InteractiveForm(schema.attributes).initiate_dialog()
387
388
        message = '---\nDo you want to preview the config in an editor before saving?'
389
        description = 'Secrets will be shown in plain text.'
390
        preview_dialog = interactive.Question(message, {'default': 'y',
391
                                                        'description': description})
392
        if preview_dialog.read() == 'y':
393
            try:
394
                contents = yaml.safe_dump(config, indent=4, default_flow_style=False)
395
                modified = editor.edit(contents=contents)
396
                config = yaml.safe_load(modified)
397
            except editor.EditorError as e:
398
                print(str(e))
399
400
        message = '---\nDo you want me to save it?'
401
        save_dialog = interactive.Question(message, {'default': 'y'})
402
        if save_dialog.read() == 'n':
403
            raise OperationFailureException('Interrupted')
404
405
        config_item = Config(pack=args.name, values=config)
406
        result = self.app.client.managers['Config'].update(config_item, **kwargs)
407
408
        return result
409
410
    def run_and_print(self, args, **kwargs):
411
        try:
412
            instance = self.run(args, **kwargs)
413
            if not instance:
414
                raise Exception("Configuration failed")
415
            self.print_output(instance, table.PropertyValueTable,
416
                              attributes=['all'], json=args.json, yaml=args.yaml)
417
        except (KeyboardInterrupt, SystemExit):
418
            raise OperationFailureException('Interrupted')
419
        except Exception as e:
420
            if self.app.client.debug:
421
                raise
422
423
            message = e.message or str(e)
424
            print('ERROR: %s' % (message))
425
            raise OperationFailureException(message)
426