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
|
|||
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 |
It is generally discouraged to redefine built-ins as this makes code very hard to read.