Просмотр исходного кода

Extend templatization ability to additional_headers field

Jeremy Stretch 6 лет назад
Родитель
Сommit
9a532b1eb2
3 измененных файлов с 52 добавлено и 37 удалено
  1. 26 8
      netbox/extras/models.py
  2. 1 1
      netbox/extras/tests/test_webhooks.py
  3. 25 28
      netbox/extras/webhooks_worker.py

+ 26 - 8
netbox/extras/models.py

@@ -1,3 +1,4 @@
+import json
 from collections import OrderedDict
 from datetime import date
 
@@ -12,6 +13,7 @@ from django.http import HttpResponse
 from django.template import Template, Context
 from django.urls import reverse
 from django.utils.text import slugify
+from rest_framework.utils.encoders import JSONEncoder
 from taggit.models import TagBase, GenericTaggedItemBase
 
 from utilities.fields import ColorField
@@ -92,8 +94,9 @@ class Webhook(models.Model):
     )
     additional_headers = models.TextField(
         blank=True,
-        help_text="User supplied headers which should be added to the request in addition to the HTTP content type. "
-                  "Headers are supplied as key/value pairs in a JSON object."
+        help_text="User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
+                  "Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
+                  "support with the same context as the request body (below)."
     )
     body_template = models.TextField(
         blank=True,
@@ -139,14 +142,29 @@ class Webhook(models.Model):
 
         if not self.ssl_verification and self.ca_file_path:
             raise ValidationError({
-                'ca_file_path': 'Do not specify a CA certificate file if SSL verification is dissabled.'
+                'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.'
             })
 
-        # Verify that JSON data is provided as an object
-        if self.additional_headers and type(self.additional_headers) is not dict:
-            raise ValidationError({
-                'additional_headers': 'Header JSON data must be in object form. Example: {"X-API-KEY": "abc123"}'
-            })
+    def render_headers(self, context):
+        """
+        Render additional_headers and return a dict of Header: Value pairs.
+        """
+        if not self.additional_headers:
+            return {}
+        ret = {}
+        data = render_jinja2(self.additional_headers, context)
+        for line in data.splitlines():
+            header, value = line.split(':')
+            ret[header.strip()] = value.strip()
+        return ret
+
+    def render_body(self, context):
+        if self.body_template:
+            return render_jinja2(self.body_template, context)
+        elif self.http_content_type == HTTP_CONTENT_TYPE_JSON:
+            return json.dumps(context, cls=JSONEncoder)
+        else:
+            return context
 
 
 #

+ 1 - 1
netbox/extras/tests/test_webhooks.py

@@ -34,7 +34,7 @@ class WebhookTest(APITestCase):
         DUMMY_SECRET = "LOOKATMEIMASECRETSTRING"
 
         webhooks = Webhook.objects.bulk_create((
-            Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers={'X-Foo': 'Bar'}),
+            Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
             Webhook(name='Site Update Webhook', type_update=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
             Webhook(name='Site Delete Webhook', type_delete=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
         ))

+ 25 - 28
netbox/extras/webhooks_worker.py

@@ -1,14 +1,10 @@
-import json
 import logging
 
 import requests
 from django_rq import job
 from jinja2.exceptions import TemplateError
-from rest_framework.utils.encoders import JSONEncoder
 
-from utilities.utils import render_jinja2
 from .choices import ObjectChangeActionChoices
-from .constants import HTTP_CONTENT_TYPE_JSON
 from .webhooks import generate_signature
 
 logger = logging.getLogger('netbox.webhooks_worker')
@@ -28,55 +24,56 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
         'data': data
     }
 
-    # Build HTTP headers
+    # Build the headers for the HTTP request
     headers = {
         'Content-Type': webhook.http_content_type,
     }
-    if webhook.additional_headers:
-        headers.update(webhook.additional_headers)
+    try:
+        headers.update(webhook.render_headers(context))
+    except (TemplateError, ValueError) as e:
+        logger.error("Error parsing HTTP headers for webhook {}: {}".format(webhook, e))
+        raise e
 
+    # Render the request body
+    try:
+        body = webhook.render_body(context)
+    except TemplateError as e:
+        logger.error("Error rendering request body for webhook {}: {}".format(webhook, e))
+        raise e
+
+    # Prepare the HTTP request
     params = {
         'method': 'POST',
         'url': webhook.payload_url,
-        'headers': headers
+        'headers': headers,
+        'data': body,
     }
-
     logger.info(
         "Sending webhook to {}: {} {}".format(params['url'], context['model'], context['event'])
     )
+    logger.debug(params)
+    try:
+        prepared_request = requests.Request(**params).prepare()
+    except requests.exceptions.RequestException as e:
+        logger.error("Error forming HTTP request: {}".format(e))
+        raise e
 
-    # Construct the request body. If a template has been defined, use it. Otherwise, dump the context as either JSON
-    # or form data.
-    if webhook.body_template:
-        try:
-            params['data'] = render_jinja2(webhook.body_template, context)
-        except TemplateError as e:
-            logger.error("Error rendering request body: {}".format(e))
-            return
-    elif webhook.http_content_type == HTTP_CONTENT_TYPE_JSON:
-        params['data'] = json.dumps(context, cls=JSONEncoder)
-    else:
-        params['data'] = context
-
-    prepared_request = requests.Request(**params).prepare()
-
+    # If a secret key is defined, sign the request with a hash of the key and its content
     if webhook.secret != '':
-        # Sign the request with a hash of the secret key and its content.
         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)
 
-    logger.debug(params)
-
     if 200 <= response.status_code <= 299:
         logger.info("Request succeeded; response status {}".format(response.status_code))
         return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
     else:
-        logger.error("Request failed; response status {}: {}".format(response.status_code, response.content))
+        logger.warning("Request failed; response status {}: {}".format(response.status_code, response.content))
         raise requests.exceptions.RequestException(
             "Status {} returned with content '{}', webhook FAILED to process.".format(
                 response.status_code, response.content