Passed
Push — develop ( 71cfc9...483b2a )
by Plexxi
07:06 queued 03:42
created

ResourceController._get_one_by_scope_and_name()   A

Complexity

Conditions 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 19
rs 9.4285
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
# pylint: disable=no-member
17
18
import abc
19
import copy
20
21
from mongoengine import ValidationError
22
import pecan
23
from pecan import rest
24
import six
25
from six.moves import http_client
26
27
from st2common.models.api.base import jsexpose
28
from st2common import log as logging
29
from st2common.models.system.common import InvalidResourceReferenceError
30
from st2common.models.system.common import ResourceReference
31
from st2common.exceptions.db import StackStormDBObjectNotFoundError
32
33
LOG = logging.getLogger(__name__)
34
35
RESERVED_QUERY_PARAMS = {
36
    'id': 'id',
37
    'name': 'name',
38
    'sort': 'order_by'
39
}
40
41
42
@six.add_metaclass(abc.ABCMeta)
43
class ResourceController(rest.RestController):
44
    model = abc.abstractproperty
45
    access = abc.abstractproperty
46
    supported_filters = abc.abstractproperty
47
48
    # Default kwargs passed to "APIClass.from_model" method
49
    from_model_kwargs = {}
50
51
    # Maximum value of limit which can be specified by user
52
    max_limit = 100
53
54
    query_options = {
55
        'sort': []
56
    }
57
58
    # A list of optional transformation functions for user provided filter values
59
    filter_transform_functions = {}
60
61
    # Method responsible for retrieving an instance of the corresponding model DB object
62
    # Note: This method should throw StackStormDBObjectNotFoundError if the corresponding DB
63
    # object doesn't exist
64
    get_one_db_method = None
65
66
    def __init__(self):
67
        self.supported_filters = copy.deepcopy(self.__class__.supported_filters)
68
        self.supported_filters.update(RESERVED_QUERY_PARAMS)
69
        self.get_one_db_method = self._get_by_name_or_id
70
71
    @jsexpose()
72
    def get_all(self, **kwargs):
73
        return self._get_all(**kwargs)
74
75
    @jsexpose(arg_types=[str])
76
    def get_one(self, id):
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...
77
        return self._get_one_by_id(id=id)
78
79
    def _get_all(self, exclude_fields=None, sort=None, offset=0, limit=None, query_options=None,
80
                 from_model_kwargs=None, **kwargs):
81
        """
82
        :param exclude_fields: A list of object fields to exclude.
83
        :type exclude_fields: ``list``
84
        """
85
        kwargs = copy.deepcopy(kwargs)
86
87
        exclude_fields = exclude_fields or []
88
        query_options = query_options if query_options else self.query_options
89
90
        # TODO: Why do we use comma delimited string, user can just specify
91
        # multiple values using ?sort=foo&sort=bar and we get a list back
92
        sort = sort.split(',') if sort else []
93
94
        db_sort_values = []
95
        for sort_key in sort:
96
            if sort_key.startswith('-'):
97
                direction = '-'
98
                sort_key = sort_key[1:]
99
            elif sort_key.startswith('+'):
100
                direction = '+'
101
                sort_key = sort_key[1:]
102
            else:
103
                direction = ''
104
105
            if sort_key not in self.supported_filters:
106
                # Skip unsupported sort key
107
                continue
108
109
            sort_value = direction + self.supported_filters[sort_key]
110
            db_sort_values.append(sort_value)
111
112
        default_sort_values = copy.copy(query_options.get('sort'))
113
        kwargs['sort'] = db_sort_values if db_sort_values else default_sort_values
114
115
        # TODO: To protect us from DoS, we need to make max_limit mandatory
116
        offset = int(offset)
117
118
        if limit and int(limit) > self.max_limit:
119
            limit = self.max_limit
120
        eop = offset + int(limit) if limit else None
121
122
        filters = {}
123
124
        for k, v in six.iteritems(self.supported_filters):
125
            filter_value = kwargs.get(k, None)
126
127
            if not filter_value:
128
                continue
129
130
            value_transform_function = self.filter_transform_functions.get(k, None)
131
            value_transform_function = value_transform_function or (lambda value: value)
132
            filter_value = value_transform_function(value=filter_value)
133
134
            filters['__'.join(v.split('.'))] = filter_value
135
136
        extra = {
137
            'filters': filters,
138
            'sort': sort,
139
            'offset': offset,
140
            'limit': limit
141
        }
142
        LOG.info('GET all %s with filters=%s' % (pecan.request.path, filters), extra=extra)
143
144
        instances = self.access.query(exclude_fields=exclude_fields, **filters)
145
        if limit == 1:
146
            # Perform the filtering on the DB side
147
            instances = instances.limit(limit)
148
149
        if limit:
150
            pecan.response.headers['X-Limit'] = str(limit)
151
        pecan.response.headers['X-Total-Count'] = str(instances.count())
152
153
        from_model_kwargs = from_model_kwargs or {}
154
        from_model_kwargs.update(self._get_from_model_kwargs_for_request(request=pecan.request))
155
156
        result = []
157
        for instance in instances[offset:eop]:
158
            item = self.model.from_model(instance, **from_model_kwargs)
159
            result.append(item)
160
161
        return result
162
163
    def _get_one(self, id, exclude_fields=None):
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...
164
        # Note: This is here for backward compatibility reasons
165
        return self._get_one_by_id(id=id, exclude_fields=exclude_fields)
166
167
    def _get_one_by_id(self, id, exclude_fields=None, from_model_kwargs=None):
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...
168
        """
169
        :param exclude_fields: A list of object fields to exclude.
170
        :type exclude_fields: ``list``
171
        """
172
173
        LOG.info('GET %s with id=%s', pecan.request.path, id)
174
175
        instance = self._get_by_id(resource_id=id, exclude_fields=exclude_fields)
176
177
        if not instance:
178
            msg = 'Unable to identify resource with id "%s".' % id
179
            pecan.abort(http_client.NOT_FOUND, msg)
180
181
        from_model_kwargs = from_model_kwargs or {}
182
        from_model_kwargs.update(self._get_from_model_kwargs_for_request(request=pecan.request))
183
        result = self.model.from_model(instance, **from_model_kwargs)
184
        LOG.debug('GET %s with id=%s, client_result=%s', pecan.request.path, id, result)
185
186
        return result
187
188
    def _get_one_by_name_or_id(self, name_or_id, exclude_fields=None, from_model_kwargs=None):
189
        """
190
        :param exclude_fields: A list of object fields to exclude.
191
        :type exclude_fields: ``list``
192
        """
193
194
        LOG.info('GET %s with name_or_id=%s', pecan.request.path, name_or_id)
195
196
        instance = self._get_by_name_or_id(name_or_id=name_or_id, exclude_fields=exclude_fields)
197
198
        if not instance:
199
            msg = 'Unable to identify resource with name_or_id "%s".' % (name_or_id)
200
            pecan.abort(http_client.NOT_FOUND, msg)
201
202
        from_model_kwargs = from_model_kwargs or {}
203
        from_model_kwargs.update(self._get_from_model_kwargs_for_request(request=pecan.request))
204
        result = self.model.from_model(instance, **from_model_kwargs)
205
        LOG.debug('GET %s with name_or_id=%s, client_result=%s', pecan.request.path, id, result)
206
207
        return result
208
209
    def _get_one_by_pack_ref(self, pack_ref, exclude_fields=None, from_model_kwargs=None):
210
        LOG.info('GET %s with pack_ref=%s', pecan.request.path, pack_ref)
211
212
        instance = self._get_by_pack_ref(pack_ref=pack_ref, exclude_fields=exclude_fields)
213
214
        if not instance:
215
            msg = 'Unable to identify resource with pack_ref "%s".' % (pack_ref)
216
            pecan.abort(http_client.NOT_FOUND, msg)
217
218
        from_model_kwargs = from_model_kwargs or {}
219
        from_model_kwargs.update(self._get_from_model_kwargs_for_request(request=pecan.request))
220
        result = self.model.from_model(instance, **from_model_kwargs)
221
        LOG.debug('GET %s with pack_ref=%s, client_result=%s', pecan.request.path, id, result)
222
223
        return result
224
225
    def _get_by_id(self, resource_id, exclude_fields=None):
226
        try:
227
            resource_db = self.access.get(id=resource_id, exclude_fields=exclude_fields)
228
        except ValidationError:
229
            resource_db = None
230
231
        return resource_db
232
233
    def _get_by_name(self, resource_name, exclude_fields=None):
234
        try:
235
            resource_db = self.access.get(name=resource_name, exclude_fields=exclude_fields)
236
        except Exception:
237
            resource_db = None
238
239
        return resource_db
240
241
    def _get_by_pack_ref(self, pack_ref, exclude_fields=None):
242
        try:
243
            resource_db = self.access.get(pack=pack_ref, exclude_fields=exclude_fields)
244
        except Exception:
245
            resource_db = None
246
247
        return resource_db
248
249
    def _get_by_name_or_id(self, name_or_id, exclude_fields=None):
250
        """
251
        Retrieve resource object by an id of a name.
252
        """
253
        resource_db = self._get_by_id(resource_id=name_or_id, exclude_fields=exclude_fields)
254
255
        if not resource_db:
256
            # Try name
257
            resource_db = self._get_by_name(resource_name=name_or_id, exclude_fields=exclude_fields)
258
259
        if not resource_db:
260
            msg = 'Resource with a name or id "%s" not found' % (name_or_id)
261
            raise StackStormDBObjectNotFoundError(msg)
262
263
        return resource_db
264
265
    def _get_from_model_kwargs_for_request(self, request):
266
        """
267
        Retrieve kwargs which are passed to "LiveActionAPI.model" method.
268
269
        :param request: Pecan request object.
270
271
        :rtype: ``dict``
272
        """
273
        return self.from_model_kwargs
274
275
    def _get_one_by_scope_and_name(self, scope, name, from_model_kwargs=None):
276
        """
277
        Retrieve an item given scope and name. Only KeyValuePair now has concept of 'scope'.
278
279
        :param scope: Scope the key belongs to.
280
        :type scope: ``str``
281
282
        :param name: Name of the key.
283
        :type name: ``str``
284
        """
285
        instance = self.access.get_by_scope_and_name(scope=scope, name=name)
286
        if not instance:
287
            msg = 'KeyValuePair with name: %s and scope: %s not found in db.' % (name, scope)
288
            raise StackStormDBObjectNotFoundError(msg)
289
        from_model_kwargs = from_model_kwargs or {}
290
        result = self.model.from_model(instance, **from_model_kwargs)
291
        LOG.debug('GET with scope=%s and name=%s, client_result=%s', scope, name, result)
292
293
        return result
294
295
296
class ContentPackResourceController(ResourceController):
297
    include_reference = False
298
299
    def __init__(self):
300
        super(ContentPackResourceController, self).__init__()
301
        self.get_one_db_method = self._get_by_ref_or_id
302
303
    @jsexpose(arg_types=[str])
304
    def get_one(self, ref_or_id):
305
        return self._get_one(ref_or_id)
306
307
    @jsexpose()
308
    def get_all(self, **kwargs):
309
        return self._get_all(**kwargs)
310
311
    def _get_one(self, ref_or_id, exclude_fields=None):
312
        LOG.info('GET %s with ref_or_id=%s', pecan.request.path, ref_or_id)
313
314
        try:
315
            instance = self._get_by_ref_or_id(ref_or_id=ref_or_id, exclude_fields=exclude_fields)
316
        except Exception as e:
317
            LOG.exception(e.message)
318
            pecan.abort(http_client.NOT_FOUND, e.message)
319
            return
320
321
        from_model_kwargs = self._get_from_model_kwargs_for_request(request=pecan.request)
322
        result = self.model.from_model(instance, **from_model_kwargs)
323
        if result and self.include_reference:
324
            pack = getattr(result, 'pack', None)
325
            name = getattr(result, 'name', None)
326
            result.ref = ResourceReference(pack=pack, name=name).ref
327
328
        LOG.debug('GET %s with ref_or_id=%s, client_result=%s',
329
                  pecan.request.path, ref_or_id, result)
330
331
        return result
332
333
    def _get_all(self, **kwargs):
334
        result = super(ContentPackResourceController, self)._get_all(**kwargs)
335
336
        if self.include_reference:
337
            for item in result:
338
                pack = getattr(item, 'pack', None)
339
                name = getattr(item, 'name', None)
340
                item.ref = ResourceReference(pack=pack, name=name).ref
341
342
        return result
343
344
    def _get_by_ref_or_id(self, ref_or_id, exclude_fields=None):
345
        """
346
        Retrieve resource object by an id of a reference.
347
348
        Note: This method throws StackStormDBObjectNotFoundError exception if the object is not
349
        found in the database.
350
        """
351
352
        if ResourceReference.is_resource_reference(ref_or_id):
353
            # references always contain a dot and id's can't contain it
354
            is_reference = True
355
        else:
356
            is_reference = False
357
358
        if is_reference:
359
            resource_db = self._get_by_ref(resource_ref=ref_or_id, exclude_fields=exclude_fields)
360
        else:
361
            resource_db = self._get_by_id(resource_id=ref_or_id, exclude_fields=exclude_fields)
362
363
        if not resource_db:
364
            msg = 'Resource with a reference or id "%s" not found' % (ref_or_id)
365
            raise StackStormDBObjectNotFoundError(msg)
366
367
        return resource_db
368
369
    def _get_by_ref(self, resource_ref, exclude_fields=None):
370
        try:
371
            ref = ResourceReference.from_string_reference(ref=resource_ref)
372
        except Exception:
373
            return None
374
375
        resource_db = self.access.query(name=ref.name, pack=ref.pack,
376
                                        exclude_fields=exclude_fields).first()
377
        return resource_db
378
379
    def _get_filters(self, **kwargs):
380
        filters = copy.deepcopy(kwargs)
381
        ref = filters.get('ref', None)
382
383
        if ref:
384
            try:
385
                ref_obj = ResourceReference.from_string_reference(ref=ref)
386
            except InvalidResourceReferenceError:
387
                raise
388
389
            filters['name'] = ref_obj.name
390
            filters['pack'] = ref_obj.pack
391
            del filters['ref']
392
393
        return filters
394