signals.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import importlib
  2. import logging
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.core.exceptions import ImproperlyConfigured, ValidationError
  5. from django.db.models.fields.reverse_related import ManyToManyRel
  6. from django.db.models.signals import m2m_changed, post_save, pre_delete
  7. from django.dispatch import receiver, Signal
  8. from django.utils.translation import gettext_lazy as _
  9. from django_prometheus.models import model_deletes, model_inserts, model_updates
  10. from core.choices import ObjectChangeActionChoices
  11. from core.events import *
  12. from core.models import ObjectChange, ObjectType
  13. from core.signals import job_end, job_start
  14. from extras.events import process_event_rules
  15. from extras.models import EventRule, Notification, Subscription
  16. from netbox.config import get_config
  17. from netbox.context import current_request, events_queue
  18. from netbox.models.features import ChangeLoggingMixin
  19. from netbox.registry import registry
  20. from netbox.signals import post_clean
  21. from utilities.exceptions import AbortRequest
  22. from .events import enqueue_event
  23. from .models import CustomField, TaggedItem
  24. from .validators import CustomValidator
  25. def run_validators(instance, validators):
  26. """
  27. Run the provided iterable of validators for the instance.
  28. """
  29. request = current_request.get()
  30. for validator in validators:
  31. # Loading a validator class by dotted path
  32. if type(validator) is str:
  33. module, cls = validator.rsplit('.', 1)
  34. validator = getattr(importlib.import_module(module), cls)()
  35. # Constructing a new instance on the fly from a ruleset
  36. elif type(validator) is dict:
  37. validator = CustomValidator(validator)
  38. elif not issubclass(validator.__class__, CustomValidator):
  39. raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
  40. validator(instance, request)
  41. #
  42. # Change logging/webhooks
  43. #
  44. # Define a custom signal that can be sent to clear any queued events
  45. clear_events = Signal()
  46. @receiver((post_save, m2m_changed))
  47. def handle_changed_object(sender, instance, **kwargs):
  48. """
  49. Fires when an object is created or updated.
  50. """
  51. m2m_changed = False
  52. if not hasattr(instance, 'to_objectchange'):
  53. return
  54. # Get the current request, or bail if not set
  55. request = current_request.get()
  56. if request is None:
  57. return
  58. # Determine the type of change being made
  59. if kwargs.get('created'):
  60. event_type = OBJECT_CREATED
  61. elif 'created' in kwargs:
  62. event_type = OBJECT_UPDATED
  63. elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
  64. # m2m_changed with objects added or removed
  65. m2m_changed = True
  66. event_type = OBJECT_UPDATED
  67. else:
  68. return
  69. # Create/update an ObjectChange record for this change
  70. action = {
  71. OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE,
  72. OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE,
  73. OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE,
  74. }[event_type]
  75. objectchange = instance.to_objectchange(action)
  76. # If this is a many-to-many field change, check for a previous ObjectChange instance recorded
  77. # for this object by this request and update it
  78. if m2m_changed and (
  79. prev_change := ObjectChange.objects.filter(
  80. changed_object_type=ContentType.objects.get_for_model(instance),
  81. changed_object_id=instance.pk,
  82. request_id=request.id
  83. ).first()
  84. ):
  85. prev_change.postchange_data = objectchange.postchange_data
  86. prev_change.save()
  87. elif objectchange and objectchange.has_changes:
  88. objectchange.user = request.user
  89. objectchange.request_id = request.id
  90. objectchange.save()
  91. # Ensure that we're working with fresh M2M assignments
  92. if m2m_changed:
  93. instance.refresh_from_db()
  94. # Enqueue the object for event processing
  95. queue = events_queue.get()
  96. enqueue_event(queue, instance, request.user, request.id, event_type)
  97. events_queue.set(queue)
  98. # Increment metric counters
  99. if event_type == OBJECT_CREATED:
  100. model_inserts.labels(instance._meta.model_name).inc()
  101. elif event_type == OBJECT_UPDATED:
  102. model_updates.labels(instance._meta.model_name).inc()
  103. @receiver(pre_delete)
  104. def handle_deleted_object(sender, instance, **kwargs):
  105. """
  106. Fires when an object is deleted.
  107. """
  108. # Run any deletion protection rules for the object. Note that this must occur prior
  109. # to queueing any events for the object being deleted, in case a validation error is
  110. # raised, causing the deletion to fail.
  111. model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
  112. validators = get_config().PROTECTION_RULES.get(model_name, [])
  113. try:
  114. run_validators(instance, validators)
  115. except ValidationError as e:
  116. raise AbortRequest(
  117. _("Deletion is prevented by a protection rule: {message}").format(message=e)
  118. )
  119. # Get the current request, or bail if not set
  120. request = current_request.get()
  121. if request is None:
  122. return
  123. # Record an ObjectChange if applicable
  124. if hasattr(instance, 'to_objectchange'):
  125. if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
  126. instance.snapshot()
  127. objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
  128. objectchange.user = request.user
  129. objectchange.request_id = request.id
  130. objectchange.save()
  131. # Django does not automatically send an m2m_changed signal for the reverse direction of a
  132. # many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
  133. # trigger one manually. We do this by checking for any reverse M2M relationships on the
  134. # instance being deleted, and explicitly call .remove() on the remote M2M field to delete
  135. # the association. This triggers an m2m_changed signal with the `post_remove` action type
  136. # for the forward direction of the relationship, ensuring that the change is recorded.
  137. for relation in instance._meta.related_objects:
  138. if type(relation) is not ManyToManyRel:
  139. continue
  140. related_model = relation.related_model
  141. related_field_name = relation.remote_field.name
  142. if not issubclass(related_model, ChangeLoggingMixin):
  143. # We only care about triggering the m2m_changed signal for models which support
  144. # change logging
  145. continue
  146. for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
  147. obj.snapshot() # Ensure the change record includes the "before" state
  148. getattr(obj, related_field_name).remove(instance)
  149. # Enqueue the object for event processing
  150. queue = events_queue.get()
  151. enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED)
  152. events_queue.set(queue)
  153. # Increment metric counters
  154. model_deletes.labels(instance._meta.model_name).inc()
  155. @receiver(clear_events)
  156. def clear_events_queue(sender, **kwargs):
  157. """
  158. Delete any queued events (e.g. because of an aborted bulk transaction)
  159. """
  160. logger = logging.getLogger('events')
  161. logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
  162. events_queue.set({})
  163. #
  164. # Custom fields
  165. #
  166. def handle_cf_added_obj_types(instance, action, pk_set, **kwargs):
  167. """
  168. Handle the population of default/null values when a CustomField is added to one or more ContentTypes.
  169. """
  170. if action == 'post_add':
  171. instance.populate_initial_data(ContentType.objects.filter(pk__in=pk_set))
  172. def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
  173. """
  174. Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
  175. """
  176. if action == 'post_remove':
  177. instance.remove_stale_data(ContentType.objects.filter(pk__in=pk_set))
  178. def handle_cf_renamed(instance, created, **kwargs):
  179. """
  180. Handle the renaming of custom field data on objects when a CustomField is renamed.
  181. """
  182. if not created and instance.name != instance._name:
  183. instance.rename_object_data(old_name=instance._name, new_name=instance.name)
  184. def handle_cf_deleted(instance, **kwargs):
  185. """
  186. Handle the cleanup of old custom field data when a CustomField is deleted.
  187. """
  188. instance.remove_stale_data(instance.object_types.all())
  189. post_save.connect(handle_cf_renamed, sender=CustomField)
  190. pre_delete.connect(handle_cf_deleted, sender=CustomField)
  191. m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.object_types.through)
  192. m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.object_types.through)
  193. #
  194. # Custom validation
  195. #
  196. @receiver(post_clean)
  197. def run_save_validators(sender, instance, **kwargs):
  198. """
  199. Run any custom validation rules for the model prior to calling save().
  200. """
  201. model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
  202. validators = get_config().CUSTOM_VALIDATORS.get(model_name, [])
  203. run_validators(instance, validators)
  204. #
  205. # Tags
  206. #
  207. @receiver(m2m_changed, sender=TaggedItem)
  208. def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
  209. """
  210. Validate that any Tags being assigned to the instance are not restricted to non-applicable object types.
  211. """
  212. if action != 'pre_add':
  213. return
  214. ct = ObjectType.objects.get_for_model(instance)
  215. # Retrieve any applied Tags that are restricted to certain object types
  216. for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'):
  217. if ct not in tag.object_types.all():
  218. raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")
  219. #
  220. # Event rules
  221. #
  222. @receiver(job_start)
  223. def process_job_start_event_rules(sender, **kwargs):
  224. """
  225. Process event rules for jobs starting.
  226. """
  227. event_rules = EventRule.objects.filter(
  228. event_types__contains=[JOB_STARTED],
  229. enabled=True,
  230. object_types=sender.object_type
  231. )
  232. username = sender.user.username if sender.user else None
  233. process_event_rules(
  234. event_rules=event_rules,
  235. object_type=sender.object_type,
  236. event_type=JOB_STARTED,
  237. data=sender.data,
  238. username=username
  239. )
  240. @receiver(job_end)
  241. def process_job_end_event_rules(sender, **kwargs):
  242. """
  243. Process event rules for jobs terminating.
  244. """
  245. event_rules = EventRule.objects.filter(
  246. event_types__contains=[JOB_COMPLETED],
  247. enabled=True,
  248. object_types=sender.object_type
  249. )
  250. username = sender.user.username if sender.user else None
  251. process_event_rules(
  252. event_rules=event_rules,
  253. object_type=sender.object_type,
  254. event_type=JOB_COMPLETED,
  255. data=sender.data,
  256. username=username
  257. )
  258. #
  259. # Notifications
  260. #
  261. @receiver(post_save)
  262. def notify_object_changed(sender, instance, created, raw, **kwargs):
  263. if created or raw:
  264. return
  265. # Skip unsupported object types
  266. ct = ContentType.objects.get_for_model(instance)
  267. if ct.model not in registry['model_features']['notifications'].get(ct.app_label, []):
  268. return
  269. # Find all subscribed Users
  270. subscribed_users = Subscription.objects.filter(object_type=ct, object_id=instance.pk).values_list('user', flat=True)
  271. if not subscribed_users:
  272. return
  273. # Delete any existing Notifications for the object
  274. Notification.objects.filter(object_type=ct, object_id=instance.pk, user__in=subscribed_users).delete()
  275. # Create Notifications for Subscribers
  276. Notification.objects.bulk_create([
  277. Notification(user_id=user, object=instance, event_type=OBJECT_UPDATED)
  278. for user in subscribed_users
  279. ])