Преглед изворни кода

Add conditions for webhooks

jeremystretch пре 4 година
родитељ
комит
78ecc8673c

+ 1 - 1
netbox/extras/api/serializers.py

@@ -61,7 +61,7 @@ class WebhookSerializer(ValidatedModelSerializer):
         fields = [
             'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
             'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
-            'ssl_verification', 'ca_file_path',
+            'conditions', 'ssl_verification', 'ca_file_path',
         ]
 
 

+ 1 - 1
netbox/extras/forms/bulk_edit.py

@@ -137,7 +137,7 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
     )
 
     class Meta:
-        nullable_fields = ['secret', 'ca_file_path']
+        nullable_fields = ['secret', 'conditions', 'ca_file_path']
 
 
 class TagBulkEditForm(BootstrapMixin, BulkEditForm):

+ 1 - 0
netbox/extras/forms/models.py

@@ -102,6 +102,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
             ('HTTP Request', (
                 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
             )),
+            ('Conditions', ('conditions',)),
             ('SSL', ('ssl_verification', 'ca_file_path')),
         )
         widgets = {

+ 18 - 0
netbox/extras/migrations/0063_webhook_conditions.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2021-10-22 20:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0062_clear_secrets_changelog'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='webhook',
+            name='conditions',
+            field=models.JSONField(blank=True, null=True),
+        ),
+    ]

+ 14 - 4
netbox/extras/models/models.py

@@ -9,11 +9,12 @@ from django.db import models
 from django.http import HttpResponse
 from django.urls import reverse
 from django.utils import timezone
-from django.utils.formats import date_format, time_format
+from django.utils.formats import date_format
 from rest_framework.utils.encoders import JSONEncoder
 
 from extras.choices import *
 from extras.constants import *
+from extras.conditions import ConditionSet
 from extras.utils import extras_features, FeatureQuery, image_upload
 from netbox.models import BigIDModel, ChangeLoggedModel
 from utilities.querysets import RestrictedQuerySet
@@ -107,6 +108,11 @@ class Webhook(ChangeLoggedModel):
                   "the secret as the key. The secret is not transmitted in "
                   "the request."
     )
+    conditions = models.JSONField(
+        blank=True,
+        null=True,
+        help_text="A set of conditions which determine whether the webhook will be generated."
+    )
     ssl_verification = models.BooleanField(
         default=True,
         verbose_name='SSL verification',
@@ -138,9 +144,13 @@ class Webhook(ChangeLoggedModel):
 
         # At least one action type must be selected
         if not self.type_create and not self.type_delete and not self.type_update:
-            raise ValidationError(
-                "You must select at least one type: create, update, and/or delete."
-            )
+            raise ValidationError("At least one type must be selected: create, update, and/or delete.")
+
+        if self.conditions:
+            try:
+                ConditionSet(self.conditions)
+            except ValueError as e:
+                raise ValidationError({'conditions': e})
 
         # CA file path requires SSL verification enabled
         if not self.ssl_verification and self.ca_file_path:

+ 1 - 0
netbox/extras/tests/test_views.py

@@ -145,6 +145,7 @@ class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'payload_url': 'http://example.com/?x',
             'http_method': 'GET',
             'http_content_type': 'application/foo',
+            'conditions': None,
         }
 
         cls.csv_data = (

+ 15 - 12
netbox/extras/webhooks_worker.py

@@ -6,6 +6,7 @@ from django_rq import job
 from jinja2.exceptions import TemplateError
 
 from .choices import ObjectChangeActionChoices
+from .conditions import ConditionSet
 from .webhooks import generate_signature
 
 logger = logging.getLogger('netbox.webhooks_worker')
@@ -16,6 +17,12 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
     """
     Make a POST request to the defined Webhook
     """
+    # Evaluate webhook conditions (if any)
+    if webhook.conditions:
+        if not ConditionSet(webhook.conditions).eval(data):
+            return
+
+    # Prepare context data for headers & body templates
     context = {
         'event': dict(ObjectChangeActionChoices)[event].lower(),
         'timestamp': timestamp,
@@ -33,14 +40,14 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
     try:
         headers.update(webhook.render_headers(context))
     except (TemplateError, ValueError) as e:
-        logger.error("Error parsing HTTP headers for webhook {}: {}".format(webhook, 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("Error rendering request body for webhook {}: {}".format(webhook, e))
+        logger.error(f"Error rendering request body for webhook {webhook}: {e}")
         raise e
 
     # Prepare the HTTP request
@@ -51,15 +58,13 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
         'data': body.encode('utf8'),
     }
     logger.info(
-        "Sending {} request to {} ({} {})".format(
-            params['method'], params['url'], context['model'], context['event']
-        )
+        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("Error forming HTTP request: {}".format(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
@@ -74,12 +79,10 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
         response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
 
     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)
+        logger.info(f"Request succeeded; response status {response.status_code}")
+        return f"Status {response.status_code} returned, webhook successfully processed."
     else:
-        logger.warning("Request failed; response status {}: {}".format(response.status_code, response.content))
+        logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
         raise requests.exceptions.RequestException(
-            "Status {} returned with content '{}', webhook FAILED to process.".format(
-                response.status_code, response.content
-            )
+            f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
         )