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

st2common/st2common/services/triggers.py (1 issue)

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
18
import six
19
20
from st2common import log as logging
21
from st2common.constants.triggers import CRON_TIMER_TRIGGER_REF
22
from st2common.exceptions.sensors import TriggerTypeRegistrationException
23
from st2common.exceptions.triggers import TriggerDoesNotExistException
24
from st2common.exceptions.db import StackStormDBObjectNotFoundError
25
from st2common.exceptions.db import StackStormDBObjectConflictError
26
from st2common.models.api.trigger import (TriggerAPI, TriggerTypeAPI)
27
from st2common.models.system.common import ResourceReference
28
from st2common.persistence.trigger import (Trigger, TriggerType)
29
30
__all__ = [
31
    'add_trigger_models',
32
33
    'get_trigger_db_by_ref',
34
    'get_trigger_db_by_id',
35
    'get_trigger_db_by_uid',
36
    'get_trigger_db_by_ref_or_dict',
37
    'get_trigger_db_given_type_and_params',
38
    'get_trigger_type_db',
39
40
    'create_trigger_db',
41
    'create_trigger_type_db',
42
43
    'create_or_update_trigger_db',
44
    'create_or_update_trigger_type_db'
45
]
46
47
LOG = logging.getLogger(__name__)
48
49
50
def get_trigger_db_given_type_and_params(type=None, parameters=None):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in type.

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

Loading history...
51
    try:
52
        parameters = parameters or {}
53
        trigger_dbs = Trigger.query(type=type,
54
                                    parameters=parameters)
55
56
        trigger_db = trigger_dbs[0] if len(trigger_dbs) > 0 else None
57
58
        # NOTE: This is a work-around which we might be able to remove once we upgrade
59
        # pymongo and mongoengine
60
        # Work around for cron-timer when in some scenarios finding an object fails when Python
61
        # value types are unicode :/
62
        is_cron_trigger = (type == CRON_TIMER_TRIGGER_REF)
63
        has_parameters = bool(parameters)
64
65
        if not trigger_db and six.PY2 and is_cron_trigger and has_parameters:
66
            non_unicode_literal_parameters = {}
67
            for key, value in six.iteritems(parameters):
68
                key = key.encode('utf-8')
69
70
                if isinstance(value, six.text_type):
71
                    # We only encode unicode to str
72
                    value = value.encode('utf-8')
73
74
                non_unicode_literal_parameters[key] = value
75
            parameters = non_unicode_literal_parameters
76
77
            trigger_dbs = Trigger.query(type=type,
78
                                        parameters=non_unicode_literal_parameters).no_cache()
79
80
            # Note: We need to directly access the object, using len or accessing the query set
81
            # twice won't work - there seems to bug a bug with cursor where accessing it twice
82
            # will throw an exception
83
            try:
84
                trigger_db = trigger_dbs[0]
85
            except IndexError:
86
                trigger_db = None
87
88
        if not parameters and not trigger_db:
89
            # We need to do double query because some TriggeDB objects without
90
            # parameters have "parameters" attribute stored in the db and others
91
            # don't
92
            trigger_db = Trigger.query(type=type, parameters=None).first()
93
94
        return trigger_db
95
    except StackStormDBObjectNotFoundError as e:
96
        LOG.debug('Database lookup for type="%s" parameters="%s" resulted ' +
97
                  'in exception : %s.', type, parameters, e, exc_info=True)
98
        return None
99
100
101
def get_trigger_db_by_ref_or_dict(trigger):
102
    """
103
    Retrieve TriggerDB object based on the trigger reference of based on a
104
    provided dictionary with trigger attributes.
105
    """
106
    # TODO: This is nasty, this should take a unique reference and not a dict
107
    if isinstance(trigger, six.string_types):
108
        trigger_db = get_trigger_db_by_ref(trigger)
109
    else:
110
        # If id / uid is available we try to look up Trigger by id. This way we can avoid bug in
111
        # pymongo / mongoengine related to "parameters" dictionary lookups
112
        trigger_id = trigger.get('id', None)
113
        trigger_uid = trigger.get('uid', None)
114
115
        # TODO: Remove parameters dictionary look up when we can confirm each trigger dictionary
116
        # passed to this method always contains id or uid
117
        if trigger_id:
118
            LOG.debug('Looking up TriggerDB by id: %s', trigger_id)
119
            trigger_db = get_trigger_db_by_id(id=trigger_id)
120
        elif trigger_uid:
121
            LOG.debug('Looking up TriggerDB by uid: %s', trigger_uid)
122
            trigger_db = get_trigger_db_by_uid(uid=trigger_uid)
123
        else:
124
            # Last resort - look it up by parameters
125
            trigger_type = trigger.get('type', None)
126
            parameters = trigger.get('parameters', {})
127
128
            LOG.debug('Looking up TriggerDB by type and parameters: type=%s, parameters=%s',
129
                      trigger_type, parameters)
130
            trigger_db = get_trigger_db_given_type_and_params(type=trigger_type,
131
                                                              parameters=parameters)
132
133
    return trigger_db
134
135
136
def get_trigger_db_by_id(id):
137
    """
138
    Returns the trigger object from db given a trigger id.
139
140
    :param ref: Reference to the trigger db object.
141
    :type ref: ``str``
142
143
    :rtype: ``object``
144
    """
145
    try:
146
        return Trigger.get_by_id(id)
147
    except StackStormDBObjectNotFoundError as e:
148
        LOG.debug('Database lookup for id="%s" resulted in exception : %s.',
149
                  id, e, exc_info=True)
150
151
    return None
152
153
154
def get_trigger_db_by_uid(uid):
155
    """
156
    Returns the trigger object from db given a trigger uid.
157
158
    :param ref: Reference to the trigger db object.
159
    :type ref: ``str``
160
161
    :rtype: ``object``
162
    """
163
    try:
164
        return Trigger.get_by_uid(uid)
165
    except StackStormDBObjectNotFoundError as e:
166
        LOG.debug('Database lookup for uid="%s" resulted in exception : %s.',
167
                  uid, e, exc_info=True)
168
169
    return None
170
171
172
def get_trigger_db_by_ref(ref):
173
    """
174
    Returns the trigger object from db given a string ref.
175
176
    :param ref: Reference to the trigger db object.
177
    :type ref: ``str``
178
179
    :rtype trigger_type: ``object``
180
    """
181
    try:
182
        return Trigger.get_by_ref(ref)
183
    except StackStormDBObjectNotFoundError as e:
184
        LOG.debug('Database lookup for ref="%s" resulted ' +
185
                  'in exception : %s.', ref, e, exc_info=True)
186
187
    return None
188
189
190
def _get_trigger_db(trigger):
191
    # TODO: This method should die in a fire
192
    # XXX: Do not make this method public.
193
194
    if isinstance(trigger, dict):
195
        name = trigger.get('name', None)
196
        pack = trigger.get('pack', None)
197
198
        if name and pack:
199
            ref = ResourceReference.to_string_reference(name=name, pack=pack)
200
            return get_trigger_db_by_ref(ref)
201
        return get_trigger_db_given_type_and_params(type=trigger['type'],
202
                                                    parameters=trigger.get('parameters', {}))
203
    else:
204
        raise Exception('Unrecognized object')
205
206
207
def get_trigger_type_db(ref):
208
    """
209
    Returns the trigger type object from db given a string ref.
210
211
    :param ref: Reference to the trigger type db object.
212
    :type ref: ``str``
213
214
    :rtype trigger_type: ``object``
215
    """
216
    try:
217
        return TriggerType.get_by_ref(ref)
218
    except StackStormDBObjectNotFoundError as e:
219
        LOG.debug('Database lookup for ref="%s" resulted ' +
220
                  'in exception : %s.', ref, e, exc_info=True)
221
222
    return None
223
224
225
def _get_trigger_dict_given_rule(rule):
226
    trigger = rule.trigger
227
    trigger_dict = {}
228
    triggertype_ref = ResourceReference.from_string_reference(trigger.get('type'))
229
    trigger_dict['pack'] = trigger_dict.get('pack', triggertype_ref.pack)
230
    trigger_dict['type'] = triggertype_ref.ref
231
    trigger_dict['parameters'] = rule.trigger.get('parameters', {})
232
233
    return trigger_dict
234
235
236
def create_trigger_db(trigger_api):
237
    # TODO: This is used only in trigger API controller. We should get rid of this.
238
    trigger_ref = ResourceReference.to_string_reference(name=trigger_api.name,
239
                                                        pack=trigger_api.pack)
240
    trigger_db = get_trigger_db_by_ref(trigger_ref)
241
    if not trigger_db:
242
        trigger_db = TriggerAPI.to_model(trigger_api)
243
        LOG.debug('Verified trigger and formulated TriggerDB=%s', trigger_db)
244
        trigger_db = Trigger.add_or_update(trigger_db)
245
    return trigger_db
246
247
248
def create_or_update_trigger_db(trigger):
249
    """
250
    Create a new TriggerDB model if one doesn't exist yet or update existing
251
    one.
252
253
    :param trigger: Trigger info.
254
    :type trigger: ``dict``
255
    """
256
    assert isinstance(trigger, dict)
257
258
    existing_trigger_db = _get_trigger_db(trigger)
259
260
    if existing_trigger_db:
261
        is_update = True
262
    else:
263
        is_update = False
264
265
    trigger_api = TriggerAPI(**trigger)
266
    trigger_api.validate()
267
    trigger_db = TriggerAPI.to_model(trigger_api)
268
269
    if is_update:
270
        trigger_db.id = existing_trigger_db.id
271
272
    trigger_db = Trigger.add_or_update(trigger_db)
273
274
    extra = {'trigger_db': trigger_db}
275
276
    if is_update:
277
        LOG.audit('Trigger updated. Trigger.id=%s' % (trigger_db.id), extra=extra)
278
    else:
279
        LOG.audit('Trigger created. Trigger.id=%s' % (trigger_db.id), extra=extra)
280
281
    return trigger_db
282
283
284
def create_trigger_db_from_rule(rule):
285
    trigger_dict = _get_trigger_dict_given_rule(rule)
286
    existing_trigger_db = _get_trigger_db(trigger_dict)
287
    # For simple triggertypes (triggertype with no parameters), we create a trigger when
288
    # registering triggertype. So if we hit the case that there is no trigger in db but
289
    # parameters is empty, then this case is a run time error.
290
    if not trigger_dict.get('parameters', {}) and not existing_trigger_db:
291
        raise TriggerDoesNotExistException(
292
            'A simple trigger should have been created when registering '
293
            'triggertype. Cannot create trigger: %s.' % (trigger_dict))
294
295
    if not existing_trigger_db:
296
        trigger_db = create_or_update_trigger_db(trigger_dict)
297
    else:
298
        trigger_db = existing_trigger_db
299
300
    # Special reference counting for trigger with parameters.
301
    # if trigger_dict.get('parameters', None):
302
    #     Trigger.update(trigger_db, inc__ref_count=1)
303
304
    return trigger_db
305
306
307
def increment_trigger_ref_count(rule_api):
308
    """
309
    Given the rule figures out the TriggerType with parameter and increments
310
    reference count on the appropriate Trigger.
311
312
    :param rule_api: Rule object used to infer the Trigger.
313
    :type rule_api: ``RuleApi``
314
    """
315
    trigger_dict = _get_trigger_dict_given_rule(rule_api)
316
317
    # Special reference counting for trigger with parameters.
318
    if trigger_dict.get('parameters', None):
319
        trigger_db = _get_trigger_db(trigger_dict)
320
        Trigger.update(trigger_db, inc__ref_count=1)
321
322
323
def cleanup_trigger_db_for_rule(rule_db):
324
    # rule.trigger is actually trigger_db ref.
325
    existing_trigger_db = get_trigger_db_by_ref(rule_db.trigger)
326
    if not existing_trigger_db or not existing_trigger_db.parameters:
327
        # nothing to be done here so moving on.
328
        LOG.debug('ref_count decrement for %s not required.', existing_trigger_db)
329
        return
330
    Trigger.update(existing_trigger_db, dec__ref_count=1)
331
    Trigger.delete_if_unreferenced(existing_trigger_db)
332
333
334
def create_trigger_type_db(trigger_type):
335
    """
336
    Creates a trigger type db object in the db given trigger_type definition as dict.
337
338
    :param trigger_type: Trigger type model.
339
    :type trigger_type: ``dict``
340
341
    :rtype: ``object``
342
    """
343
    trigger_type_api = TriggerTypeAPI(**trigger_type)
344
    trigger_type_api.validate()
345
    ref = ResourceReference.to_string_reference(name=trigger_type_api.name,
346
                                                pack=trigger_type_api.pack)
347
    trigger_type_db = get_trigger_type_db(ref)
348
349
    if not trigger_type_db:
350
        trigger_type_db = TriggerTypeAPI.to_model(trigger_type_api)
351
        LOG.debug('verified trigger and formulated TriggerDB=%s', trigger_type_db)
352
        trigger_type_db = TriggerType.add_or_update(trigger_type_db)
353
    return trigger_type_db
354
355
356
def create_shadow_trigger(trigger_type_db):
357
    """
358
    Create a shadow trigger for TriggerType with no parameters.
359
    """
360
    trigger_type_ref = trigger_type_db.get_reference().ref
361
362
    if trigger_type_db.parameters_schema:
363
        LOG.debug('Skip shadow trigger for TriggerType with parameters %s.', trigger_type_ref)
364
        return None
365
366
    trigger = {'name': trigger_type_db.name,
367
               'pack': trigger_type_db.pack,
368
               'type': trigger_type_ref,
369
               'parameters': {}}
370
371
    return create_or_update_trigger_db(trigger)
372
373
374
def create_or_update_trigger_type_db(trigger_type):
375
    """
376
    Create or update a trigger type db object in the db given trigger_type definition as dict.
377
378
    :param trigger_type: Trigger type model.
379
    :type trigger_type: ``dict``
380
381
    :rtype: ``object``
382
    """
383
    assert isinstance(trigger_type, dict)
384
385
    trigger_type_api = TriggerTypeAPI(**trigger_type)
386
    trigger_type_api.validate()
387
    trigger_type_api = TriggerTypeAPI.to_model(trigger_type_api)
388
389
    ref = ResourceReference.to_string_reference(name=trigger_type_api.name,
390
                                                pack=trigger_type_api.pack)
391
392
    existing_trigger_type_db = get_trigger_type_db(ref)
393
    if existing_trigger_type_db:
394
        is_update = True
395
    else:
396
        is_update = False
397
398
    if is_update:
399
        trigger_type_api.id = existing_trigger_type_db.id
400
401
    try:
402
        trigger_type_db = TriggerType.add_or_update(trigger_type_api)
403
    except StackStormDBObjectConflictError:
404
        # Operation is idempotent and trigger could have already been created by
405
        # another process. Ignore object already exists because it simply means
406
        # there was a race and object is already in the database.
407
        trigger_type_db = get_trigger_type_db(ref)
408
        is_update = True
409
410
    extra = {'trigger_type_db': trigger_type_db}
411
412
    if is_update:
413
        LOG.audit('TriggerType updated. TriggerType.id=%s' % (trigger_type_db.id), extra=extra)
414
    else:
415
        LOG.audit('TriggerType created. TriggerType.id=%s' % (trigger_type_db.id), extra=extra)
416
417
    return trigger_type_db
418
419
420
def _create_trigger_type(pack, name, description=None, payload_schema=None,
421
                         parameters_schema=None, tags=None):
422
    trigger_type = {
423
        'name': name,
424
        'pack': pack,
425
        'description': description,
426
        'payload_schema': payload_schema,
427
        'parameters_schema': parameters_schema,
428
        'tags': tags
429
    }
430
431
    return create_or_update_trigger_type_db(trigger_type=trigger_type)
432
433
434
def _validate_trigger_type(trigger_type):
435
    """
436
    XXX: We need validator objects that define the required and optional fields.
437
    For now, manually check them.
438
    """
439
    required_fields = ['name']
440
    for field in required_fields:
441
        if field not in trigger_type:
442
            raise TriggerTypeRegistrationException('Invalid trigger type. Missing field "%s"' %
443
                                                   (field))
444
445
446
def _create_trigger(trigger_type):
447
    """
448
    :param trigger_type: TriggerType db object.
449
    :type trigger_type: :class:`TriggerTypeDB`
450
    """
451
    if hasattr(trigger_type, 'parameters_schema') and not trigger_type['parameters_schema']:
452
        trigger_dict = {
453
            'name': trigger_type.name,
454
            'pack': trigger_type.pack,
455
            'type': trigger_type.get_reference().ref
456
        }
457
458
        try:
459
            return create_or_update_trigger_db(trigger=trigger_dict)
460
        except:
461
            LOG.exception('Validation failed for Trigger=%s.', trigger_dict)
462
            raise TriggerTypeRegistrationException(
463
                'Unable to create Trigger for TriggerType=%s.' % trigger_type.name)
464
    else:
465
        LOG.debug('Won\'t create Trigger object as TriggerType %s expects ' +
466
                  'parameters.', trigger_type)
467
        return None
468
469
470
def _add_trigger_models(trigger_type):
471
    pack = trigger_type['pack']
472
    description = trigger_type['description'] if 'description' in trigger_type else ''
473
    payload_schema = trigger_type['payload_schema'] if 'payload_schema' in trigger_type else {}
474
    parameters_schema = trigger_type['parameters_schema'] \
475
        if 'parameters_schema' in trigger_type else {}
476
    tags = trigger_type.get('tags', [])
477
478
    trigger_type = _create_trigger_type(
479
        pack=pack,
480
        name=trigger_type['name'],
481
        description=description,
482
        payload_schema=payload_schema,
483
        parameters_schema=parameters_schema,
484
        tags=tags
485
    )
486
    trigger = _create_trigger(trigger_type=trigger_type)
487
    return (trigger_type, trigger)
488
489
490
def add_trigger_models(trigger_types):
491
    """
492
    Register trigger types.
493
494
    :param trigger_types: A list of triggers to register.
495
    :type trigger_types: ``list`` of ``dict``
496
497
    :rtype: ``list`` of ``tuple`` (trigger_type, trigger)
498
    """
499
    [r for r in (_validate_trigger_type(trigger_type)
500
     for trigger_type in trigger_types) if r is not None]
501
502
    result = []
503
    for trigger_type in trigger_types:
504
        item = _add_trigger_models(trigger_type=trigger_type)
505
506
        if item:
507
            result.append(item)
508
509
    return result
510