diff --git a/netbox/extras/events.py b/netbox/extras/events.py index 05352b7d1..1d7a7ed64 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -108,7 +108,7 @@ def process_event_rules(event_rules, model_name, event, data, username, snapshot # Enqueue the task rq_queue.enqueue( - "extras.webhooks_worker.process_webhook", + "extras.webhooks.send_webhook", **params ) diff --git a/netbox/extras/tests/test_event_rules.py b/netbox/extras/tests/test_event_rules.py index ed64ba891..549c33478 100644 --- a/netbox/extras/tests/test_event_rules.py +++ b/netbox/extras/tests/test_event_rules.py @@ -11,8 +11,7 @@ from django.urls import reverse from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices from extras.events import enqueue_object, flush_events, serialize_for_event from extras.models import EventRule, Tag, Webhook -from extras.webhooks import generate_signature -from extras.webhooks_worker import process_webhook +from extras.webhooks import generate_signature, send_webhook from requests import Session from rest_framework import status from utilities.testing import APITestCase @@ -331,7 +330,7 @@ class EventRuleTest(APITestCase): self.assertEqual(job.kwargs['snapshots']['prechange']['name'], sites[i].name) self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo']) - def test_webhooks_worker(self): + def test_send_webhook(self): request_id = uuid.uuid4() def dummy_send(_, request, **kwargs): @@ -376,4 +375,4 @@ class EventRuleTest(APITestCase): # Patch the Session object with our dummy_send() method, then process the webhook for sending with patch.object(Session, 'send', dummy_send) as mock_send: - process_webhook(**job.kwargs) + send_webhook(**job.kwargs) diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index a48a8038b..53ec161d7 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -1,5 +1,15 @@ import hashlib import hmac +import logging + +import requests +from django.conf import settings +from django_rq import job +from jinja2.exceptions import TemplateError + +from .constants import WEBHOOK_EVENT_TYPES + +logger = logging.getLogger('netbox.webhooks') def generate_signature(request_body, secret): @@ -12,3 +22,79 @@ def generate_signature(request_body, secret): digestmod=hashlib.sha512 ) return hmac_prep.hexdigest() + + +@job('default') +def send_webhook(event_rule, model_name, event, data, timestamp, username, request_id=None, snapshots=None): + """ + Make a POST request to the defined Webhook + """ + webhook = event_rule.action_object + + # Prepare context data for headers & body templates + context = { + 'event': WEBHOOK_EVENT_TYPES[event], + 'timestamp': timestamp, + 'model': model_name, + 'username': username, + 'request_id': request_id, + 'data': data, + } + if snapshots: + context.update({ + 'snapshots': snapshots + }) + + # Build the headers for the HTTP request + headers = { + 'Content-Type': webhook.http_content_type, + } + try: + headers.update(webhook.render_headers(context)) + except (TemplateError, ValueError) as e: + logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}") + raise e + + # Render the request body + try: + body = webhook.render_body(context) + except TemplateError as e: + logger.error(f"Error rendering request body for webhook {webhook}: {e}") + raise e + + # Prepare the HTTP request + params = { + 'method': webhook.http_method, + 'url': webhook.render_payload_url(context), + 'headers': headers, + 'data': body.encode('utf8'), + } + logger.info( + f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})" + ) + logger.debug(params) + try: + prepared_request = requests.Request(**params).prepare() + except requests.exceptions.RequestException as e: + logger.error(f"Error forming HTTP request: {e}") + raise e + + # If a secret key is defined, sign the request with a hash of the key and its content + if webhook.secret != '': + prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret) + + # Send the request + with requests.Session() as session: + session.verify = webhook.ssl_verification + if webhook.ca_file_path: + session.verify = webhook.ca_file_path + response = session.send(prepared_request, proxies=settings.HTTP_PROXIES) + + if 200 <= response.status_code <= 299: + logger.info(f"Request succeeded; response status {response.status_code}") + return f"Status {response.status_code} returned, webhook successfully processed." + else: + logger.warning(f"Request failed; response status {response.status_code}: {response.content}") + raise requests.exceptions.RequestException( + f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process." + ) diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index 4d6d8135e..77535fafa 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -1,87 +1,10 @@ -import logging +import warnings -import requests -from django.conf import settings -from django_rq import job -from jinja2.exceptions import TemplateError - -from .constants import WEBHOOK_EVENT_TYPES -from .webhooks import generate_signature - -logger = logging.getLogger('netbox.webhooks_worker') +from .webhooks import send_webhook as process_webhook -@job('default') -def process_webhook(event_rule, model_name, event, data, timestamp, username, request_id=None, snapshots=None): - """ - Make a POST request to the defined Webhook - """ - webhook = event_rule.action_object - - # Prepare context data for headers & body templates - context = { - 'event': WEBHOOK_EVENT_TYPES[event], - 'timestamp': timestamp, - 'model': model_name, - 'username': username, - 'request_id': request_id, - 'data': data, - } - if snapshots: - context.update({ - 'snapshots': snapshots - }) - - # Build the headers for the HTTP request - headers = { - 'Content-Type': webhook.http_content_type, - } - try: - headers.update(webhook.render_headers(context)) - except (TemplateError, ValueError) as e: - logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}") - raise e - - # Render the request body - try: - body = webhook.render_body(context) - except TemplateError as e: - logger.error(f"Error rendering request body for webhook {webhook}: {e}") - raise e - - # Prepare the HTTP request - params = { - 'method': webhook.http_method, - 'url': webhook.render_payload_url(context), - 'headers': headers, - 'data': body.encode('utf8'), - } - logger.info( - f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})" - ) - logger.debug(params) - try: - prepared_request = requests.Request(**params).prepare() - except requests.exceptions.RequestException as e: - logger.error(f"Error forming HTTP request: {e}") - raise e - - # If a secret key is defined, sign the request with a hash of the key and its content - if webhook.secret != '': - prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret) - - # Send the request - with requests.Session() as session: - session.verify = webhook.ssl_verification - if webhook.ca_file_path: - session.verify = webhook.ca_file_path - response = session.send(prepared_request, proxies=settings.HTTP_PROXIES) - - if 200 <= response.status_code <= 299: - logger.info(f"Request succeeded; response status {response.status_code}") - return f"Status {response.status_code} returned, webhook successfully processed." - else: - logger.warning(f"Request failed; response status {response.status_code}: {response.content}") - raise requests.exceptions.RequestException( - f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process." - ) +# TODO: Remove in v4.0 +warnings.warn( + f"webhooks_worker.process_webhook has been moved to webhooks.send_webhook.", + DeprecationWarning +)