Completed
Push — master ( 9329a6...6e30e7 )
by Edward
02:26
created

ResourceBranch.__init__()   B

Complexity

Conditions 5

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
c 1
b 0
f 0
dl 0
loc 42
rs 8.0894
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
        cmd_map = {
75
            "list": ResourceListCommand,
76
            "get": ResourceGetCommand,
77
            "create": ResourceCreateCommand,
78
            "update": ResourceUpdateCommand,
79
            "delete": ResourceDeleteCommand,
80
            "enable": ResourceEnableCommand,
81
            "disable": ResourceDisableCommand
82
        }
83
        for cmd, cmd_class in cmd_map.items():
84
            if cmd not in commands:
85
                commands[cmd] = cmd_class
86
87
        # Instantiate commands.
88
        args = [self.resource, self.app, self.subparsers]
89
        self.commands['list'] = commands['list'](*args)
90
        self.commands['get'] = commands['get'](*args)
91
92
        if not read_only:
93
            self.commands['create'] = commands['create'](*args)
94
            self.commands['update'] = commands['update'](*args)
95
            self.commands['delete'] = commands['delete'](*args)
96
97
        if has_disable:
98
            self.commands['enable'] = commands['enable'](*args)
99
            self.commands['disable'] = commands['disable'](*args)
100
101
102
@six.add_metaclass(abc.ABCMeta)
103
class ResourceCommand(commands.Command):
104
    pk_argument_name = None
105
106
    def __init__(self, resource, *args, **kwargs):
107
108
        has_token_opt = kwargs.pop('has_token_opt', True)
109
110
        super(ResourceCommand, self).__init__(*args, **kwargs)
111
112
        self.resource = resource
113
114
        if has_token_opt:
115
            self.parser.add_argument('-t', '--token', dest='token',
116
                                     help='Access token for user authentication. '
117
                                          'Get ST2_AUTH_TOKEN from the environment '
118
                                          'variables by default.')
119
            self.parser.add_argument('--api-key', dest='api_key',
120
                                     help='Api Key for user authentication. '
121
                                          'Get ST2_API_KEY from the environment '
122
                                          'variables by default.')
123
124
        # Formatter flags
125
        self.parser.add_argument('-j', '--json',
126
                                 action='store_true', dest='json',
127
                                 help='Prints output in JSON format.')
128
        self.parser.add_argument('-y', '--yaml',
129
                                 action='store_true', dest='yaml',
130
                                 help='Prints output in YAML format.')
131
132
    @property
133
    def manager(self):
134
        return self.app.client.managers[self.resource.__name__]
135
136
    @property
137
    def arg_name_for_resource_id(self):
138
        resource_name = self.resource.get_display_name().lower()
139
        return '%s-id' % resource_name.replace(' ', '-')
140
141
    def print_not_found(self, name):
142
        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...
143
               (self.resource.get_display_name(), name))
144
145
    def get_resource(self, name_or_id, **kwargs):
146
        pk_argument_name = self.pk_argument_name
147
148
        if pk_argument_name == 'name_or_id':
149
            instance = self.get_resource_by_name_or_id(name_or_id=name_or_id, **kwargs)
150
        elif pk_argument_name == 'ref_or_id':
151
            instance = self.get_resource_by_ref_or_id(ref_or_id=name_or_id, **kwargs)
152
        else:
153
            instance = self.get_resource_by_pk(pk=name_or_id, **kwargs)
154
155
        return instance
156
157
    def get_resource_by_pk(self, pk, **kwargs):
158
        """
159
        Retrieve resource by a primary key.
160
        """
161
        try:
162
            instance = self.manager.get_by_id(pk, **kwargs)
163
        except Exception as e:
164
            traceback.print_exc()
165
            # Hack for "Unauthorized" exceptions, we do want to propagate those
166
            response = getattr(e, 'response', None)
167
            status_code = getattr(response, 'status_code', None)
168
            if status_code and status_code == httplib.UNAUTHORIZED:
169
                raise e
170
171
            instance = None
172
173
        return instance
174
175
    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...
176
        instance = self.get_resource_by_pk(pk=id, **kwargs)
177
178
        if not instance:
179
            message = ('Resource with id "%s" doesn\'t exist.' % (id))
180
            raise ResourceNotFoundError(message)
181
        return instance
182
183
    def get_resource_by_name(self, name, **kwargs):
184
        """
185
        Retrieve resource by name.
186
        """
187
        instance = self.manager.get_by_name(name, **kwargs)
188
        return instance
189
190
    def get_resource_by_name_or_id(self, name_or_id, **kwargs):
191
        instance = self.get_resource_by_name(name=name_or_id, **kwargs)
192
        if not instance:
193
            instance = self.get_resource_by_pk(pk=name_or_id, **kwargs)
194
195
        if not instance:
196
            message = ('Resource with id or name "%s" doesn\'t exist.' %
197
                       (name_or_id))
198
            raise ResourceNotFoundError(message)
199
        return instance
200
201
    def get_resource_by_ref_or_id(self, ref_or_id, **kwargs):
202
        instance = self.manager.get_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
203
204
        if not instance:
205
            message = ('Resource with id or reference "%s" doesn\'t exist.' %
206
                       (ref_or_id))
207
            raise ResourceNotFoundError(message)
208
        return instance
209
210
    @abc.abstractmethod
211
    def run(self, args, **kwargs):
212
        raise NotImplementedError
213
214
    @abc.abstractmethod
215
    def run_and_print(self, args, **kwargs):
216
        raise NotImplementedError
217
218
    def _get_metavar_for_argument(self, argument):
219
        return argument.replace('_', '-')
220
221
    def _get_help_for_argument(self, resource, argument):
222
        argument_display_name = argument.title()
223
        resource_display_name = resource.get_display_name().lower()
224
225
        if 'ref' in argument:
226
            result = ('Reference or ID of the %s.' % (resource_display_name))
227
        elif 'name_or_id' in argument:
228
            result = ('Name or ID of the %s.' % (resource_display_name))
229
        else:
230
            result = ('%s of the %s.' % (argument_display_name, resource_display_name))
231
232
        return result
233
234
235
class ResourceTableCommand(ResourceCommand):
236
    display_attributes = ['id', 'name', 'description']
237
238
    def __init__(self, resource, name, description, *args, **kwargs):
239
        super(ResourceTableCommand, self).__init__(resource, name, description,
240
                                                   *args, **kwargs)
241
242
        self.parser.add_argument('-a', '--attr', nargs='+',
243
                                 default=self.display_attributes,
244
                                 help=('List of attributes to include in the '
245
                                       'output. "all" will return all '
246
                                       'attributes.'))
247
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
248
                                 default=None,
249
                                 help=('Set the width of columns in output.'))
250
251
    @add_auth_token_to_kwargs_from_cli
252
    def run(self, args, **kwargs):
253
        return self.manager.get_all(**kwargs)
254
255
    def run_and_print(self, args, **kwargs):
256
        instances = self.run(args, **kwargs)
257
        self.print_output(instances, table.MultiColumnTable,
258
                          attributes=args.attr, widths=args.width,
259
                          json=args.json, yaml=args.yaml)
260
261
262
class ResourceListCommand(ResourceTableCommand):
263
    def __init__(self, resource, *args, **kwargs):
264
        super(ResourceListCommand, self).__init__(resource, 'list',
265
            'Get the list of %s.' % resource.get_plural_display_name().lower(),
266
            *args, **kwargs)
267
268
269
class ContentPackResourceListCommand(ResourceListCommand):
270
    """
271
    Base command class for use with resources which belong to a content pack.
272
    """
273
    def __init__(self, resource, *args, **kwargs):
274
        super(ContentPackResourceListCommand, self).__init__(resource,
275
                                                             *args, **kwargs)
276
277
        self.parser.add_argument('-p', '--pack', type=str,
278
                                 help=('Only return resources belonging to the'
279
                                       ' provided pack'))
280
281
    @add_auth_token_to_kwargs_from_cli
282
    def run(self, args, **kwargs):
283
        filters = {'pack': args.pack}
284
        filters.update(**kwargs)
285
        return self.manager.get_all(**filters)
286
287
288
class ResourceGetCommand(ResourceCommand):
289
    display_attributes = ['all']
290
    attribute_display_order = ['id', 'name', 'description']
291
292
    pk_argument_name = 'name_or_id'  # name of the attribute which stores resource PK
293
294
    def __init__(self, resource, *args, **kwargs):
295
        super(ResourceGetCommand, self).__init__(resource, 'get',
296
            'Get individual %s.' % resource.get_display_name().lower(),
297
            *args, **kwargs)
298
299
        argument = self.pk_argument_name
300
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
301
        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...
302
                                           argument=self.pk_argument_name)
303
304
        self.parser.add_argument(argument,
305
                                 metavar=metavar,
306
                                 help=help)
307
        self.parser.add_argument('-a', '--attr', nargs='+',
308
                                 default=self.display_attributes,
309
                                 help=('List of attributes to include in the '
310
                                       'output. "all" or unspecified will '
311
                                       'return all attributes.'))
312
313
    @add_auth_token_to_kwargs_from_cli
314
    def run(self, args, **kwargs):
315
        resource_id = getattr(args, self.pk_argument_name, None)
316
        return self.get_resource_by_id(resource_id, **kwargs)
317
318
    def run_and_print(self, args, **kwargs):
319
        try:
320
            instance = self.run(args, **kwargs)
321
            self.print_output(instance, table.PropertyValueTable,
322
                              attributes=args.attr, json=args.json, yaml=args.yaml,
323
                              attribute_display_order=self.attribute_display_order)
324
        except ResourceNotFoundError:
325
            resource_id = getattr(args, self.pk_argument_name, None)
326
            self.print_not_found(resource_id)
327
            raise OperationFailureException('Resource %s not found.' % resource_id)
328
329
330
class ContentPackResourceGetCommand(ResourceGetCommand):
331
    """
332
    Command for retrieving a single resource which belongs to a content pack.
333
334
    Note: All the resources which belong to the content pack can either be
335
    retrieved by a reference or by an id.
336
    """
337
338
    attribute_display_order = ['id', 'pack', 'name', 'description']
339
340
    pk_argument_name = 'ref_or_id'
341
342
    def get_resource(self, ref_or_id, **kwargs):
343
        return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
344
345
346
class ResourceCreateCommand(ResourceCommand):
347
348
    def __init__(self, resource, *args, **kwargs):
349
        super(ResourceCreateCommand, self).__init__(resource, 'create',
350
            'Create a new %s.' % resource.get_display_name().lower(),
351
            *args, **kwargs)
352
353
        self.parser.add_argument('file',
354
                                 help=('JSON/YAML file containing the %s to create.'
355
                                       % resource.get_display_name().lower()))
356
357
    @add_auth_token_to_kwargs_from_cli
358
    def run(self, args, **kwargs):
359
        data = load_meta_file(args.file)
360
        instance = self.resource.deserialize(data)
361
        return self.manager.create(instance, **kwargs)
362
363
    def run_and_print(self, args, **kwargs):
364
        try:
365
            instance = self.run(args, **kwargs)
366
            if not instance:
367
                raise Exception('Server did not create instance.')
368
            self.print_output(instance, table.PropertyValueTable,
369
                              attributes=['all'], json=args.json, yaml=args.yaml)
370
        except Exception as e:
371
            message = e.message or str(e)
372
            print('ERROR: %s' % (message))
373
            raise OperationFailureException(message)
374
375
376
class ResourceUpdateCommand(ResourceCommand):
377
    pk_argument_name = 'name_or_id'
378
379
    def __init__(self, resource, *args, **kwargs):
380
        super(ResourceUpdateCommand, self).__init__(resource, 'update',
381
            'Updating an existing %s.' % resource.get_display_name().lower(),
382
            *args, **kwargs)
383
384
        argument = self.pk_argument_name
385
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
386
        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...
387
                                           argument=self.pk_argument_name)
388
389
        self.parser.add_argument(argument,
390
                                 metavar=metavar,
391
                                 help=help)
392
        self.parser.add_argument('file',
393
                                 help=('JSON/YAML file containing the %s to update.'
394
                                       % resource.get_display_name().lower()))
395
396
    @add_auth_token_to_kwargs_from_cli
397
    def run(self, args, **kwargs):
398
        resource_id = getattr(args, self.pk_argument_name, None)
399
        instance = self.get_resource(resource_id, **kwargs)
400
        data = load_meta_file(args.file)
401
        modified_instance = self.resource.deserialize(data)
402
403
        if not getattr(modified_instance, 'id', None):
404
            modified_instance.id = instance.id
405
        else:
406
            if modified_instance.id != instance.id:
407
                raise Exception('The value for the %s id in the JSON/YAML file '
408
                                'does not match the ID provided in the '
409
                                'command line arguments.' %
410
                                self.resource.get_display_name().lower())
411
        return self.manager.update(modified_instance, **kwargs)
412
413
    def run_and_print(self, args, **kwargs):
414
        instance = self.run(args, **kwargs)
415
        try:
416
            self.print_output(instance, table.PropertyValueTable,
417
                              attributes=['all'], json=args.json, yaml=args.yaml)
418
        except Exception as e:
419
            print('ERROR: %s' % e.message)
420
            raise OperationFailureException(e.message)
421
422
423
class ContentPackResourceUpdateCommand(ResourceUpdateCommand):
424
    pk_argument_name = 'ref_or_id'
425
426
427
class ResourceEnableCommand(ResourceCommand):
428
    pk_argument_name = 'name_or_id'
429
430
    def __init__(self, resource, *args, **kwargs):
431
        super(ResourceEnableCommand, self).__init__(resource, 'enable',
432
            'Enable an existing %s.' % resource.get_display_name().lower(),
433
            *args, **kwargs)
434
435
        argument = self.pk_argument_name
436
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
437
        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...
438
                                           argument=self.pk_argument_name)
439
440
        self.parser.add_argument(argument,
441
                                 metavar=metavar,
442
                                 help=help)
443
444
    @add_auth_token_to_kwargs_from_cli
445
    def run(self, args, **kwargs):
446
        resource_id = getattr(args, self.pk_argument_name, None)
447
        instance = self.get_resource(resource_id, **kwargs)
448
449
        data = instance.serialize()
450
451
        if 'ref' in data:
452
            del data['ref']
453
454
        data['enabled'] = True
455
        modified_instance = self.resource.deserialize(data)
456
457
        return self.manager.update(modified_instance, **kwargs)
458
459
    def run_and_print(self, args, **kwargs):
460
        instance = self.run(args, **kwargs)
461
        self.print_output(instance, table.PropertyValueTable,
462
                          attributes=['all'], json=args.json, yaml=args.yaml)
463
464
465
class ContentPackResourceEnableCommand(ResourceEnableCommand):
466
    pk_argument_name = 'ref_or_id'
467
468
469
class ResourceDisableCommand(ResourceCommand):
470
    pk_argument_name = 'name_or_id'
471
472
    def __init__(self, resource, *args, **kwargs):
473
        super(ResourceDisableCommand, self).__init__(resource, 'disable',
474
            'Disable an existing %s.' % resource.get_display_name().lower(),
475
            *args, **kwargs)
476
477
        argument = self.pk_argument_name
478
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
479
        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...
480
                                           argument=self.pk_argument_name)
481
482
        self.parser.add_argument(argument,
483
                                 metavar=metavar,
484
                                 help=help)
485
486
    @add_auth_token_to_kwargs_from_cli
487
    def run(self, args, **kwargs):
488
        resource_id = getattr(args, self.pk_argument_name, None)
489
        instance = self.get_resource(resource_id, **kwargs)
490
491
        data = instance.serialize()
492
493
        if 'ref' in data:
494
            del data['ref']
495
496
        data['enabled'] = False
497
        modified_instance = self.resource.deserialize(data)
498
499
        return self.manager.update(modified_instance, **kwargs)
500
501
    def run_and_print(self, args, **kwargs):
502
        instance = self.run(args, **kwargs)
503
        self.print_output(instance, table.PropertyValueTable,
504
                          attributes=['all'], json=args.json, yaml=args.yaml)
505
506
507
class ContentPackResourceDisableCommand(ResourceDisableCommand):
508
    pk_argument_name = 'ref_or_id'
509
510
511
class ResourceDeleteCommand(ResourceCommand):
512
    pk_argument_name = 'name_or_id'
513
514
    def __init__(self, resource, *args, **kwargs):
515
        super(ResourceDeleteCommand, self).__init__(resource, 'delete',
516
            'Delete an existing %s.' % resource.get_display_name().lower(),
517
            *args, **kwargs)
518
519
        argument = self.pk_argument_name
520
        metavar = self._get_metavar_for_argument(argument=self.pk_argument_name)
521
        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...
522
                                           argument=self.pk_argument_name)
523
524
        self.parser.add_argument(argument,
525
                                 metavar=metavar,
526
                                 help=help)
527
528
    @add_auth_token_to_kwargs_from_cli
529
    def run(self, args, **kwargs):
530
        resource_id = getattr(args, self.pk_argument_name, None)
531
        instance = self.get_resource(resource_id, **kwargs)
532
        self.manager.delete(instance, **kwargs)
533
534
    def run_and_print(self, args, **kwargs):
535
        resource_id = getattr(args, self.pk_argument_name, None)
536
537
        try:
538
            self.run(args, **kwargs)
539
            print('Resource with id "%s" has been successfully deleted.' % (resource_id))
540
        except ResourceNotFoundError:
541
            self.print_not_found(resource_id)
542
            raise OperationFailureException('Resource %s not found.' % resource_id)
543
544
545
class ContentPackResourceDeleteCommand(ResourceDeleteCommand):
546
    """
547
    Base command class for deleting a resource which belongs to a content pack.
548
    """
549
550
    pk_argument_name = 'ref_or_id'
551
552
553
def load_meta_file(file_path):
554
    if not os.path.isfile(file_path):
555
        raise Exception('File "%s" does not exist.' % file_path)
556
557
    file_name, file_ext = os.path.splitext(file_path)
558
    if file_ext not in ALLOWED_EXTS:
559
        raise Exception('Unsupported meta type %s, file %s. Allowed: %s' %
560
                        (file_ext, file_path, ALLOWED_EXTS))
561
562
    with open(file_path, 'r') as f:
563
        return PARSER_FUNCS[file_ext](f)
564