event_rules.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import logging
  2. from django.core.exceptions import ValidationError
  3. from django.utils import timezone
  4. from django.utils.translation import gettext_lazy as _
  5. from django_rq import get_queue
  6. from netbox.config import get_config
  7. from netbox.constants import RQ_QUEUE_DEFAULT
  8. from netbox.event_rules import EventRuleAction
  9. from utilities.request import copy_safe_request
  10. from utilities.rqworker import get_rq_retry
  11. from .choices import EventRuleActionChoices
  12. from .models import NotificationGroup, Script, Webhook
  13. __all__ = (
  14. 'NotificationAction',
  15. 'ScriptAction',
  16. 'WebhookAction',
  17. )
  18. logger = logging.getLogger('netbox.events_processor')
  19. class WebhookAction(EventRuleAction):
  20. slug = EventRuleActionChoices.WEBHOOK
  21. label = _('Webhook')
  22. description = _('Send an outgoing HTTP request to a remote endpoint')
  23. object_model = Webhook
  24. object_required = True
  25. def enqueue(self, *, event_rule, event_context, action_object, action_data):
  26. # Select the appropriate RQ queue
  27. queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
  28. rq_queue = get_queue(queue_name)
  29. # Compile the task parameters
  30. params = {
  31. 'event_rule': event_rule,
  32. 'object_type': event_context['object_type'],
  33. 'event_type': event_context['event_type'],
  34. 'data': action_data,
  35. 'snapshots': event_context.get('snapshots'),
  36. 'timestamp': timezone.now().isoformat(),
  37. 'retry': get_rq_retry(),
  38. }
  39. if object_change_ids := event_context.get('object_change_ids'):
  40. params['object_change_ids'] = object_change_ids
  41. if 'request' in event_context:
  42. # Exclude FILES - webhooks don't need uploaded files,
  43. # which can cause pickle errors with Pillow.
  44. params['request'] = copy_safe_request(event_context['request'], include_files=False)
  45. # Enqueue the task
  46. rq_queue.enqueue('extras.webhooks.send_webhook', **params)
  47. def resolve_import_object(self, value):
  48. return Webhook.objects.get(name=value)
  49. class ScriptAction(EventRuleAction):
  50. slug = EventRuleActionChoices.SCRIPT
  51. label = _('Script')
  52. description = _('Execute a custom script')
  53. object_model = Script
  54. object_required = True
  55. def enqueue(self, *, event_rule, event_context, action_object, action_data):
  56. # Resolve the script from action parameters
  57. script = action_object.python_class()
  58. # Enqueue a Job to record the script's execution
  59. from extras.jobs import ScriptJob
  60. params = {
  61. 'instance': action_object,
  62. 'name': script.name,
  63. 'user': event_context['user'],
  64. 'data': action_data,
  65. 'notifications': script.notifications_default,
  66. 'job_timeout': script.job_timeout,
  67. }
  68. if 'snapshots' in event_context:
  69. params['snapshots'] = event_context['snapshots']
  70. if 'request' in event_context:
  71. params['request'] = copy_safe_request(event_context['request'], include_files=False)
  72. # Enqueue the job. If the script's Meta configuration is invalid (see #22872), log the error and skip this
  73. # action rather than allowing the exception to abort the event pipeline (and, since events are processed
  74. # in-request, the originating object change). Note this is intentionally asymmetric with the webhook
  75. # action, which lets enqueue failures propagate: script Meta is validated eagerly at enqueue and a
  76. # misconfigured script must not take down an unrelated object change.
  77. try:
  78. ScriptJob.enqueue(**params)
  79. except ValidationError as e:
  80. logger.error(
  81. "Skipping script action for event rule %s: invalid script configuration: %s",
  82. event_rule, '; '.join(e.messages)
  83. )
  84. def resolve_import_object(self, value):
  85. from extras.scripts import get_module_and_script
  86. module_name, script_name = value.split('.', 1)
  87. return get_module_and_script(module_name, script_name)[1]
  88. class NotificationAction(EventRuleAction):
  89. slug = EventRuleActionChoices.NOTIFICATION
  90. label = _('Notification')
  91. description = _('Generate a notification for one or more users or groups')
  92. object_model = NotificationGroup
  93. object_required = True
  94. def enqueue(self, *, event_rule, event_context, action_object, action_data):
  95. # Bulk-create notifications for all members of the notification group
  96. action_object.notify(
  97. object_type=event_context['object_type'],
  98. object_id=action_data['id'],
  99. object_repr=action_data.get('display'),
  100. event_type=event_context['event_type'],
  101. )
  102. def resolve_import_object(self, value):
  103. return NotificationGroup.objects.get(name=value)