Completed
Pull Request — master (#2842)
by Edward
06:44
created

ResourceTableCommand.run_and_print()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import abc
18
import six
19
import json
20
import logging
21
import httplib
22
from functools import wraps
23
import traceback
24
25
import yaml
26
27
from st2client import commands
28
from st2client.exceptions.operations import OperationFailureException
29
from st2client.formatters import table
30
31
ALLOWED_EXTS = ['.json', '.yaml', '.yml']
32
PARSER_FUNCS = {'.json': json.load, '.yml': yaml.safe_load, '.yaml': yaml.safe_load}
33
LOG = logging.getLogger(__name__)
34
35
36
def add_auth_token_to_kwargs_from_cli(func):
37
    @wraps(func)
38
    def decorate(*args, **kwargs):
39
        ns = args[1]
40
        if getattr(ns, 'token', None):
41
            kwargs['token'] = ns.token
42
        if getattr(ns, 'api_key', None):
43
            kwargs['api_key'] = ns.api_key
44
        return func(*args, **kwargs)
45
    return decorate
46
47
48
class ResourceCommandError(Exception):
49
    pass
50
51
52
class ResourceNotFoundError(Exception):
53
    pass
54
55
56
class ResourceBranch(commands.Branch):
57
58
    def __init__(self, resource, description, app, subparsers,
59
                 parent_parser=None, read_only=False, commands=None,
0 ignored issues
show
Comprehensibility Bug introduced by
commands is re-defining a name which is already available in the outer-scope (previously defined on line 27).

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...
60
                 has_disable=False):
61
62
        self.resource = resource
63
        super(ResourceBranch, self).__init__(
64
            self.resource.get_alias().lower(), description,
65
            app, subparsers, parent_parser=parent_parser)
66
67
        # Registers subcommands for managing the resource type.
68
        self.subparsers = self.parser.add_subparsers(
69
            help=('List of commands for managing %s.' %
70
                  self.resource.get_plural_display_name().lower()))
71
72
        # Resolves if commands need to be overridden.
73
        commands = commands or {}
74
        if 'list' not in commands:
75
            commands['list'] = ResourceListCommand
76
        if 'get' not in commands:
77
            commands['get'] = ResourceGetCommand
78
        if 'create' not in commands:
79
            commands['create'] = ResourceCreateCommand
80
        if 'update' not in commands:
81
            commands['update'] = ResourceUpdateCommand
82
        if 'delete' not in commands:
83
            commands['delete'] = ResourceDeleteCommand
84
85
        if 'enable' not in commands:
86
            commands['enable'] = ResourceEnableCommand
87
88
        if 'disable' not in commands:
89
            commands['disable'] = ResourceDisableCommand
90
91
        # Instantiate commands.
92
        args = [self.resource, self.app, self.subparsers]
93
        self.commands['list'] = commands['list'](*args)
94
        self.commands['get'] = commands['get'](*args)
95
96
        if not read_only:
97
            self.commands['create'] = commands['create'](*args)
98
            self.commands['update'] = commands['update'](*args)
99
            self.commands['delete'] = commands['delete'](*args)
100
101
        if has_disable:
102
            self.commands['enable'] = commands['enable'](*args)
103
            self.commands['disable'] = commands['disable'](*args)
104
105
106
@six.add_metaclass(abc.ABCMeta)
107
class ResourceCommand(commands.Command):
108
    pk_argument_name = None
109
110
    def __init__(self, resource, *args, **kwargs):
111
112
        has_token_opt = kwargs.pop('has_token_opt', True)
113
114
        super(ResourceCommand, self).__init__(*args, **kwargs)
115
116
        self.resource = resource
117
118
        if has_token_opt:
119
            self.parser.add_argument('-t', '--token', dest='token',
120
                                     help='Access token for user authentication. '
121
                                          'Get ST2_AUTH_TOKEN from the environment '
122
                                          'variables by default.')
123
            self.parser.add_argument('--api-key', dest='api_key',
124
                                     help='Api Key for user authentication. '
125
                                          'Get ST2_API_KEY from the environment '
126
                                          'variables by default.')
127
128
        # Formatter flags
129
        self.parser.add_argument('-j', '--json',
130
                                 action='store_true', dest='json',
131
                                 help='Prints output in JSON format.')
132
        self.parser.add_argument('-y', '--yaml',
133
                                 action='store_true', dest='yaml',
134
                                 help='Prints output in YAML format.')
135
136
    @property
137
    def manager(self):
138
        return self.app.client.managers[self.resource.__name__]
139
140
    @property
141
    def arg_name_for_resource_id(self):
142
        resource_name = self.resource.get_display_name().lower()
143
        return '%s-id' % resource_name.replace(' ', '-')
144
145
    def print_not_found(self, name):
146
        print ('%s "%s" is not found.\n' %
0 ignored issues
show
Coding Style introduced by
No space allowed before bracket
print ('s "s" is not found.\n' %
^
Loading history...
147
               (self.resource.get_display_name(), name))
148
149
    def get_resource(self, name_or_id, **kwargs):
150
        pk_argument_name = self.pk_argument_name
151
152
        if pk_argument_name == 'name_or_id':
153
            instance = self.get_resource_by_name_or_id(name_or_id=name_or_id, **kwargs)
154
        elif pk_argument_name == 'ref_or_id':
155
            instance = self.get_resource_by_ref_or_id(ref_or_id=name_or_id, **kwargs)
156
        else:
157
            instance = self.get_resource_by_pk(pk=name_or_id, **kwargs)
158
159
        return instance
160
161
    def get_resource_by_pk(self, pk, **kwargs):
162
        """
163
        Retrieve resource by a primary key.
164
        """
165
        try:
166
            instance = self.manager.get_by_id(pk, **kwargs)
167
        except Exception as e:
168
            traceback.print_exc()
169
            # Hack for "Unauthorized" exceptions, we do want to propagate those
170
            response = getattr(e, 'response', None)
171
            status_code = getattr(response, 'status_code', None)
172
            if status_code and status_code == httplib.UNAUTHORIZED:
173
                raise e
174
175
            instance = None
176
177
        return instance
178
179
    def get_resource_by_id(self, id, **kwargs):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in id.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
180
        instance = self.get_resource_by_pk(pk=id, **kwargs)
181
182
        if not instance:
183
            message = ('Resource with id "%s" doesn\'t exist.' % (id))
184
            raise ResourceNotFoundError(message)
185
        return instance
186
187
    def get_resource_by_name(self, name, **kwargs):
188
        """
189
        Retrieve resource by name.
190
        """
191
        instance = self.manager.get_by_name(name, **kwargs)
192
        return instance
193
194
    def get_resource_by_name_or_id(self, name_or_id, **kwargs):
195
        instance = self.get_resource_by_name(name=name_or_id, **kwargs)
196
        if not instance:
197
            instance = self.get_resource_by_pk(pk=name_or_id, **kwargs)
198
199
        if not instance:
200
            message = ('Resource with id or name "%s" doesn\'t exist.' %
201
                       (name_or_id))
202
            raise ResourceNotFoundError(message)
203
        return instance
204
205
    def get_resource_by_ref_or_id(self, ref_or_id, **kwargs):
206
        instance = self.manager.get_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
207
208
        if not instance:
209
            message = ('Resource with id or reference "%s" doesn\'t exist.' %
210
                       (ref_or_id))
211
            raise ResourceNotFoundError(message)
212
        return instance
213
214
    @abc.abstractmethod
215
    def run(self, args, **kwargs):
216
        raise NotImplementedError
217
218
    @abc.abstractmethod
219
    def run_and_print(self, args, **kwargs):
220
        raise NotImplementedError
221
222
    def _get_metavar_for_argument(self, argument):
223
        return argument.replace('_', '-')
224
225
    def _get_help_for_argument(self, resource, argument):
226
        argument_display_name = argument.title()
227
        resource_display_name = resource.get_display_name().lower()
228
229
        if 'ref' in argument:
230
            result = ('Reference or ID of the %s.' % (resource_display_name))
231
        elif 'name_or_id' in argument:
232
            result = ('Name or ID of the %s.' % (resource_display_name))
233
        else:
234
            result = ('%s of the %s.' % (argument_display_name, resource_display_name))
235
236
        return result
237
238
239
class ResourceTableCommand(ResourceCommand):
240
    display_attributes = ['id', 'name', 'description']
241
242
    def __init__(self, resource, name, description, *args, **kwargs):
243
        super(ResourceTableCommand, self).__init__(resource, name, description,
244
                                                   *args, **kwargs)
245
246
        self.parser.add_argument('-a', '--attr', nargs='+',
247
                                 default=self.display_attributes,
248
                                 help=('List of attributes to include in the '
249
                                       'output. "all" will return all '
250
                                       'attributes.'))
251
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
252
                                 default=None,
253
                                 help=('Set the width of columns in output.'))
254
255
    @add_auth_token_to_kwargs_from_cli
256
    def run(self, args, **kwargs):
257
        return self.manager.get_all(**kwargs)
258
259
    def run_and_print(self, args, **kwargs):
260
        instances = self.run(args, **kwargs)
261
        self.print_output(instances, table.MultiColumnTable,
262
                          attributes=args.attr, widths=args.width,
263
                          json=args.json, yaml=args.yaml)
264
265
266
class ResourceListCommand(ResourceTableCommand):
267
    def __init__(self, resource, *args, **kwargs):
268
        super(ResourceListCommand, self).__init__(resource, 'list',
269
            'Get the list of %s.' % resource.get_plural_display_name().lower(),
270
            *args, **kwargs)
271
272
273
class ContentPackResourceListCommand(ResourceListCommand):
274
    """
275
    Base command class for use with resources which belong to a content pack.
276
    """
277
    def __init__(self, resource, *args, **kwargs):
278
        super(ContentPackResourceListCommand, self).__init__(resource,
279
                                                             *args, **kwargs)
280
281
        self.parser.add_argument('-p', '--pack', type=str,
282
                                 help=('Only return resources belonging to the'
283
                                       ' provided pack'))
284
285
    @add_auth_token_to_kwargs_from_cli
286
    def run(self, args, **kwargs):
287
        filters = {'pack': args.pack}
288
        filters.update(**kwargs)
289
        return self.manager.get_all(**filters)
290
291
292
class ResourceGetCommand(ResourceCommand):
293
    display_attributes = ['all']
294
    attribute_display_order = ['id', 'name', 'description']
295
296
    pk_argument_name = 'name_or_id'  # name of the attribute which stores resource PK
297
298
    def __init__(self, resource, *args, **kwargs):
299
        super(ResourceGetCommand, self).__init__(resource, 'get',
300
            'Get individual %s.' % resource.get_display_name().lower(),
301
            *args, **kwargs)
302
303
        argument = self.pk_argument_name
304
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
305
        help = self._get_help_for_argument(resource=resource,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in help.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
306
                                           argument=self.pk_argument_name)
307
308
        self.parser.add_argument(argument,
309
                                 metavar=metavar,
310
                                 help=help)
311
        self.parser.add_argument('-a', '--attr', nargs='+',
312
                                 default=self.display_attributes,
313
                                 help=('List of attributes to include in the '
314
                                       'output. "all" or unspecified will '
315
                                       'return all attributes.'))
316
317
    @add_auth_token_to_kwargs_from_cli
318
    def run(self, args, **kwargs):
319
        resource_id = getattr(args, self.pk_argument_name, None)
320
        return self.get_resource_by_id(resource_id, **kwargs)
321
322
    def run_and_print(self, args, **kwargs):
323
        try:
324
            instance = self.run(args, **kwargs)
325
            self.print_output(instance, table.PropertyValueTable,
326
                              attributes=args.attr, json=args.json, yaml=args.yaml,
327
                              attribute_display_order=self.attribute_display_order)
328
        except ResourceNotFoundError:
329
            resource_id = getattr(args, self.pk_argument_name, None)
330
            self.print_not_found(resource_id)
331
            raise OperationFailureException('Resource %s not found.' % resource_id)
332
333
334
class ContentPackResourceGetCommand(ResourceGetCommand):
335
    """
336
    Command for retrieving a single resource which belongs to a content pack.
337
338
    Note: All the resources which belong to the content pack can either be
339
    retrieved by a reference or by an id.
340
    """
341
342
    attribute_display_order = ['id', 'pack', 'name', 'description']
343
344
    pk_argument_name = 'ref_or_id'
345
346
    def get_resource(self, ref_or_id, **kwargs):
347
        return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
348
349
350
class ResourceCreateCommand(ResourceCommand):
351
352
    def __init__(self, resource, *args, **kwargs):
353
        super(ResourceCreateCommand, self).__init__(resource, 'create',
354
            'Create a new %s.' % resource.get_display_name().lower(),
355
            *args, **kwargs)
356
357
        self.parser.add_argument('file',
358
                                 help=('JSON/YAML file containing the %s to create.'
359
                                       % resource.get_display_name().lower()))
360
361
    @add_auth_token_to_kwargs_from_cli
362
    def run(self, args, **kwargs):
363
        data = load_meta_file(args.file)
364
        instance = self.resource.deserialize(data)
365
        return self.manager.create(instance, **kwargs)
366
367
    def run_and_print(self, args, **kwargs):
368
        try:
369
            instance = self.run(args, **kwargs)
370
            if not instance:
371
                raise Exception('Server did not create instance.')
372
            self.print_output(instance, table.PropertyValueTable,
373
                              attributes=['all'], json=args.json, yaml=args.yaml)
374
        except Exception as e:
375
            message = e.message or str(e)
376
            print('ERROR: %s' % (message))
377
            raise OperationFailureException(message)
378
379
380
class ResourceUpdateCommand(ResourceCommand):
381
    pk_argument_name = 'name_or_id'
382
383
    def __init__(self, resource, *args, **kwargs):
384
        super(ResourceUpdateCommand, self).__init__(resource, 'update',
385
            'Updating an existing %s.' % resource.get_display_name().lower(),
386
            *args, **kwargs)
387
388
        argument = self.pk_argument_name
389
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
390
        help = self._get_help_for_argument(resource=resource,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in help.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
391
                                           argument=self.pk_argument_name)
392
393
        self.parser.add_argument(argument,
394
                                 metavar=metavar,
395
                                 help=help)
396
        self.parser.add_argument('file',
397
                                 help=('JSON/YAML file containing the %s to update.'
398
                                       % resource.get_display_name().lower()))
399
400
    @add_auth_token_to_kwargs_from_cli
401
    def run(self, args, **kwargs):
402
        resource_id = getattr(args, self.pk_argument_name, None)
403
        instance = self.get_resource(resource_id, **kwargs)
404
        data = load_meta_file(args.file)
405
        modified_instance = self.resource.deserialize(data)
406
407
        if not getattr(modified_instance, 'id', None):
408
            modified_instance.id = instance.id
409
        else:
410
            if modified_instance.id != instance.id:
411
                raise Exception('The value for the %s id in the JSON/YAML file '
412
                                'does not match the ID provided in the '
413
                                'command line arguments.' %
414
                                self.resource.get_display_name().lower())
415
        return self.manager.update(modified_instance, **kwargs)
416
417
    def run_and_print(self, args, **kwargs):
418
        instance = self.run(args, **kwargs)
419
        try:
420
            self.print_output(instance, table.PropertyValueTable,
421
                              attributes=['all'], json=args.json, yaml=args.yaml)
422
        except Exception as e:
423
            print('ERROR: %s' % e.message)
424
            raise OperationFailureException(e.message)
425
426
427
class ContentPackResourceUpdateCommand(ResourceUpdateCommand):
428
    pk_argument_name = 'ref_or_id'
429
430
431
class ResourceEnableCommand(ResourceCommand):
432
    pk_argument_name = 'name_or_id'
433
434
    def __init__(self, resource, *args, **kwargs):
435
        super(ResourceEnableCommand, self).__init__(resource, 'enable',
436
            'Enable an existing %s.' % resource.get_display_name().lower(),
437
            *args, **kwargs)
438
439
        argument = self.pk_argument_name
440
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
441
        help = self._get_help_for_argument(resource=resource,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in help.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
442
                                           argument=self.pk_argument_name)
443
444
        self.parser.add_argument(argument,
445
                                 metavar=metavar,
446
                                 help=help)
447
448
    @add_auth_token_to_kwargs_from_cli
449
    def run(self, args, **kwargs):
450
        resource_id = getattr(args, self.pk_argument_name, None)
451
        instance = self.get_resource(resource_id, **kwargs)
452
453
        data = instance.serialize()
454
455
        if 'ref' in data:
456
            del data['ref']
457
458
        data['enabled'] = True
459
        modified_instance = self.resource.deserialize(data)
460
461
        return self.manager.update(modified_instance, **kwargs)
462
463
    def run_and_print(self, args, **kwargs):
464
        instance = self.run(args, **kwargs)
465
        self.print_output(instance, table.PropertyValueTable,
466
                          attributes=['all'], json=args.json, yaml=args.yaml)
467
468
469
class ContentPackResourceEnableCommand(ResourceEnableCommand):
470
    pk_argument_name = 'ref_or_id'
471
472
473
class ResourceDisableCommand(ResourceCommand):
474
    pk_argument_name = 'name_or_id'
475
476
    def __init__(self, resource, *args, **kwargs):
477
        super(ResourceDisableCommand, self).__init__(resource, 'disable',
478
            'Disable an existing %s.' % resource.get_display_name().lower(),
479
            *args, **kwargs)
480
481
        argument = self.pk_argument_name
482
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
483
        help = self._get_help_for_argument(resource=resource,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in help.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
484
                                           argument=self.pk_argument_name)
485
486
        self.parser.add_argument(argument,
487
                                 metavar=metavar,
488
                                 help=help)
489
490
    @add_auth_token_to_kwargs_from_cli
491
    def run(self, args, **kwargs):
492
        resource_id = getattr(args, self.pk_argument_name, None)
493
        instance = self.get_resource(resource_id, **kwargs)
494
495
        data = instance.serialize()
496
497
        if 'ref' in data:
498
            del data['ref']
499
500
        data['enabled'] = False
501
        modified_instance = self.resource.deserialize(data)
502
503
        return self.manager.update(modified_instance, **kwargs)
504
505
    def run_and_print(self, args, **kwargs):
506
        instance = self.run(args, **kwargs)
507
        self.print_output(instance, table.PropertyValueTable,
508
                          attributes=['all'], json=args.json, yaml=args.yaml)
509
510
511
class ContentPackResourceDisableCommand(ResourceDisableCommand):
512
    pk_argument_name = 'ref_or_id'
513
514
515
class ResourceDeleteCommand(ResourceCommand):
516
    pk_argument_name = 'name_or_id'
517
518
    def __init__(self, resource, *args, **kwargs):
519
        super(ResourceDeleteCommand, self).__init__(resource, 'delete',
520
            'Delete an existing %s.' % resource.get_display_name().lower(),
521
            *args, **kwargs)
522
523
        argument = self.pk_argument_name
524
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
525
        help = self._get_help_for_argument(resource=resource,
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in help.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
526
                                           argument=self.pk_argument_name)
527
528
        self.parser.add_argument(argument,
529
                                 metavar=metavar,
530
                                 help=help)
531
532
    @add_auth_token_to_kwargs_from_cli
533
    def run(self, args, **kwargs):
534
        resource_id = getattr(args, self.pk_argument_name, None)
535
        instance = self.get_resource(resource_id, **kwargs)
536
        self.manager.delete(instance, **kwargs)
537
538
    def run_and_print(self, args, **kwargs):
539
        resource_id = getattr(args, self.pk_argument_name, None)
540
541
        try:
542
            self.run(args, **kwargs)
543
            print('Resource with id "%s" has been successfully deleted.' % (resource_id))
544
        except ResourceNotFoundError:
545
            self.print_not_found(resource_id)
546
            raise OperationFailureException('Resource %s not found.' % resource_id)
547
548
549
class ContentPackResourceDeleteCommand(ResourceDeleteCommand):
550
    """
551
    Base command class for deleting a resource which belongs to a content pack.
552
    """
553
554
    pk_argument_name = 'ref_or_id'
555
556
557
def load_meta_file(file_path):
558
    if not os.path.isfile(file_path):
559
        raise Exception('File "%s" does not exist.' % file_path)
560
561
    file_name, file_ext = os.path.splitext(file_path)
562
    if file_ext not in ALLOWED_EXTS:
563
        raise Exception('Unsupported meta type %s, file %s. Allowed: %s' %
564
                        (file_ext, file_path, ALLOWED_EXTS))
565
566
    with open(file_path, 'r') as f:
567
        return PARSER_FUNCS[file_ext](f)
568