webhooks_worker.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import logging
  2. import requests
  3. from django.conf import settings
  4. from django_rq import job
  5. from jinja2.exceptions import TemplateError
  6. from .choices import ObjectChangeActionChoices
  7. from .conditions import ConditionSet
  8. from .webhooks import generate_signature
  9. logger = logging.getLogger('netbox.webhooks_worker')
  10. def eval_conditions(webhook, data):
  11. """
  12. Test whether the given data meets the conditions of the webhook (if any). Return True
  13. if met or no conditions are specified.
  14. """
  15. if not webhook.conditions:
  16. return True
  17. logger.debug(f'Evaluating webhook conditions: {webhook.conditions}')
  18. if ConditionSet(webhook.conditions).eval(data):
  19. return True
  20. return False
  21. @job('default')
  22. def process_webhook(webhook, model_name, event, data, snapshots, timestamp, username, request_id):
  23. """
  24. Make a POST request to the defined Webhook
  25. """
  26. # Evaluate webhook conditions (if any)
  27. if not eval_conditions(webhook, data):
  28. return
  29. # Prepare context data for headers & body templates
  30. context = {
  31. 'event': dict(ObjectChangeActionChoices)[event].lower(),
  32. 'timestamp': timestamp,
  33. 'model': model_name,
  34. 'username': username,
  35. 'request_id': request_id,
  36. 'data': data,
  37. 'snapshots': snapshots,
  38. }
  39. # Build the headers for the HTTP request
  40. headers = {
  41. 'Content-Type': webhook.http_content_type,
  42. }
  43. try:
  44. headers.update(webhook.render_headers(context))
  45. except (TemplateError, ValueError) as e:
  46. logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
  47. raise e
  48. # Render the request body
  49. try:
  50. body = webhook.render_body(context)
  51. except TemplateError as e:
  52. logger.error(f"Error rendering request body for webhook {webhook}: {e}")
  53. raise e
  54. # Prepare the HTTP request
  55. params = {
  56. 'method': webhook.http_method,
  57. 'url': webhook.render_payload_url(context),
  58. 'headers': headers,
  59. 'data': body.encode('utf8'),
  60. }
  61. logger.info(
  62. f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
  63. )
  64. logger.debug(params)
  65. try:
  66. prepared_request = requests.Request(**params).prepare()
  67. except requests.exceptions.RequestException as e:
  68. logger.error(f"Error forming HTTP request: {e}")
  69. raise e
  70. # If a secret key is defined, sign the request with a hash of the key and its content
  71. if webhook.secret != '':
  72. prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
  73. # Send the request
  74. with requests.Session() as session:
  75. session.verify = webhook.ssl_verification
  76. if webhook.ca_file_path:
  77. session.verify = webhook.ca_file_path
  78. response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
  79. if 200 <= response.status_code <= 299:
  80. logger.info(f"Request succeeded; response status {response.status_code}")
  81. return f"Status {response.status_code} returned, webhook successfully processed."
  82. else:
  83. logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
  84. raise requests.exceptions.RequestException(
  85. f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
  86. )