Răsfoiți Sursa

Add type_job_start & type_job_end to Webhook

jeremystretch 3 ani în urmă
părinte
comite
697feed257

+ 11 - 5
docs/models/extras/webhook.md

@@ -22,11 +22,13 @@ If not selected, the webhook will be inactive.
 
 The events which will trigger the webhook. At least one event type must be selected.
 
-| Name      | Description                          |
-|-----------|--------------------------------------|
-| Creations | A new object has been created        |
-| Updates   | An existing object has been modified |
-| Deletions | An object has been deleted           |
+| Name       | Description                          |
+|------------|--------------------------------------|
+| Creations  | A new object has been created        |
+| Updates    | An existing object has been modified |
+| Deletions  | An object has been deleted           |
+| Job starts | A job for an object starts           |
+| Job ends   | A job for an object terminates       |
 
 ### URL
 
@@ -58,6 +60,10 @@ Jinja2 template for a custom request body, if desired. If not defined, NetBox wi
 
 A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
 
+### Conditions
+
+A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, the webhook will not be sent. A webhook that does not define any conditions will _always_ trigger.
+
 ### SSL Verification
 
 Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used.

+ 4 - 3
netbox/extras/api/serializers.py

@@ -68,9 +68,10 @@ class WebhookSerializer(ValidatedModelSerializer):
     class Meta:
         model = Webhook
         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',
-            'conditions', 'ssl_verification', 'ca_file_path', 'created', 'last_updated',
+            'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
+            'type_job_start', 'type_job_end', 'payload_url', 'enabled', 'http_method', 'http_content_type',
+            'additional_headers', 'body_template', 'secret', 'conditions', 'ssl_verification', 'ca_file_path',
+            'created', 'last_updated',
         ]
 
 

+ 2 - 2
netbox/extras/filtersets.py

@@ -48,8 +48,8 @@ class WebhookFilterSet(BaseFilterSet):
     class Meta:
         model = Webhook
         fields = [
-            'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
-            'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
+            'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'payload_url',
+            'enabled', 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
         ]
 
     def search(self, queryset, name, value):

+ 8 - 0
netbox/extras/forms/bulk_edit.py

@@ -140,6 +140,14 @@ class WebhookBulkEditForm(BulkEditForm):
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
+    type_job_start = forms.NullBooleanField(
+        required=False,
+        widget=BulkEditNullBooleanSelect()
+    )
+    type_job_end = forms.NullBooleanField(
+        required=False,
+        widget=BulkEditNullBooleanSelect()
+    )
     http_method = forms.ChoiceField(
         choices=add_blank_choice(WebhookHttpMethodChoices),
         required=False,

+ 3 - 3
netbox/extras/forms/bulk_import.py

@@ -116,9 +116,9 @@ class WebhookImportForm(CSVModelForm):
     class Meta:
         model = Webhook
         fields = (
-            'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
-            'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
-            'ca_file_path'
+            'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'type_job_start',
+            'type_job_end', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template',
+            'secret', 'ssl_verification', 'ca_file_path'
         )
 
 

+ 21 - 4
netbox/extras/forms/filtersets.py

@@ -222,7 +222,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
         ('Attributes', ('content_type_id', 'http_method', 'enabled')),
-        ('Events', ('type_create', 'type_update', 'type_delete')),
+        ('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
     )
     content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
@@ -244,19 +244,36 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
-        )
+        ),
+        label=_('Object creations')
     )
     type_update = forms.NullBooleanField(
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
-        )
+        ),
+        label=_('Object updates')
     )
     type_delete = forms.NullBooleanField(
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
-        )
+        ),
+        label=_('Object deletions')
+    )
+    type_job_start = forms.NullBooleanField(
+        required=False,
+        widget=forms.Select(
+            choices=BOOLEAN_WITH_BLANK_CHOICES
+        ),
+        label=_('Job starts')
+    )
+    type_job_end = forms.NullBooleanField(
+        required=False,
+        widget=forms.Select(
+            choices=BOOLEAN_WITH_BLANK_CHOICES
+        ),
+        label=_('Job terminations')
     )
 
 

+ 3 - 1
netbox/extras/forms/model_forms.py

@@ -154,7 +154,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
 
     fieldsets = (
         ('Webhook', ('name', 'content_types', 'enabled')),
-        ('Events', ('type_create', 'type_update', 'type_delete')),
+        ('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
         ('HTTP Request', (
             'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
         )),
@@ -169,6 +169,8 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
             'type_create': 'Creations',
             'type_update': 'Updates',
             'type_delete': 'Deletions',
+            'type_job_start': 'Job executions',
+            'type_job_end': 'Job terminations',
         }
         widgets = {
             'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),

+ 38 - 0
netbox/extras/migrations/0088_jobresult_webhooks.py

@@ -0,0 +1,38 @@
+# Generated by Django 4.1.7 on 2023-02-28 19:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0087_dashboard'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='webhook',
+            name='type_job_end',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='webhook',
+            name='type_job_start',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AlterField(
+            model_name='webhook',
+            name='type_create',
+            field=models.BooleanField(default=True),
+        ),
+        migrations.AlterField(
+            model_name='webhook',
+            name='type_delete',
+            field=models.BooleanField(default=True),
+        ),
+        migrations.AlterField(
+            model_name='webhook',
+            name='type_update',
+            field=models.BooleanField(default=True),
+        ),
+    ]

+ 19 - 8
netbox/extras/models/models.py

@@ -5,7 +5,6 @@ from django.conf import settings
 from django.contrib import admin
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
 from django.core.cache import cache
 from django.core.validators import MinValueValidator, ValidationError
 from django.db import models
@@ -64,16 +63,24 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
         unique=True
     )
     type_create = models.BooleanField(
-        default=False,
-        help_text=_("Call this webhook when a matching object is created.")
+        default=True,
+        help_text=_("Triggers when a matching object is created.")
     )
     type_update = models.BooleanField(
-        default=False,
-        help_text=_("Call this webhook when a matching object is updated.")
+        default=True,
+        help_text=_("Triggers when a matching object is updated.")
     )
     type_delete = models.BooleanField(
+        default=True,
+        help_text=_("Triggers when a matching object is deleted.")
+    )
+    type_job_start = models.BooleanField(
+        default=False,
+        help_text=_("Triggers when a job for a matching object is started.")
+    )
+    type_job_end = models.BooleanField(
         default=False,
-        help_text=_("Call this webhook when a matching object is deleted.")
+        help_text=_("Triggers when a job for a matching object terminates.")
     )
     payload_url = models.CharField(
         max_length=500,
@@ -159,8 +166,12 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
         super().clean()
 
         # 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("At least one type must be selected: create, update, and/or delete.")
+        if not any([
+            self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
+        ]):
+            raise ValidationError(
+                "At least one event type must be selected: create, update, delete, job_start, and/or job_end."
+            )
 
         if self.conditions:
             try:

+ 11 - 4
netbox/extras/tables/tables.py

@@ -146,6 +146,12 @@ class WebhookTable(NetBoxTable):
     type_delete = columns.BooleanColumn(
         verbose_name='Delete'
     )
+    type_job_start = columns.BooleanColumn(
+        verbose_name='Job start'
+    )
+    type_job_end = columns.BooleanColumn(
+        verbose_name='Job end'
+    )
     ssl_validation = columns.BooleanColumn(
         verbose_name='SSL Validation'
     )
@@ -153,12 +159,13 @@ class WebhookTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Webhook
         fields = (
-            'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
-            'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'created', 'last_updated',
+            'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete',
+            'type_job_start', 'type_job_end', 'http_method', 'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
+            'created', 'last_updated',
         )
         default_columns = (
-            'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method',
-            'payload_url',
+            'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'type_job_start',
+            'type_job_end', 'http_method', 'payload_url',
         )
 
 

+ 49 - 3
netbox/extras/tests/test_filtersets.py

@@ -89,12 +89,16 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
 
     @classmethod
     def setUpTestData(cls):
-        content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
+        content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device'])
 
         webhooks = (
             Webhook(
                 name='Webhook 1',
                 type_create=True,
+                type_update=False,
+                type_delete=False,
+                type_job_start=False,
+                type_job_end=False,
                 payload_url='http://example.com/?1',
                 enabled=True,
                 http_method='GET',
@@ -102,7 +106,11 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
             ),
             Webhook(
                 name='Webhook 2',
+                type_create=False,
                 type_update=True,
+                type_delete=False,
+                type_job_start=False,
+                type_job_end=False,
                 payload_url='http://example.com/?2',
                 enabled=True,
                 http_method='POST',
@@ -110,26 +118,56 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
             ),
             Webhook(
                 name='Webhook 3',
+                type_create=False,
+                type_update=False,
                 type_delete=True,
+                type_job_start=False,
+                type_job_end=False,
                 payload_url='http://example.com/?3',
                 enabled=False,
                 http_method='PATCH',
                 ssl_verification=False,
             ),
+            Webhook(
+                name='Webhook 4',
+                type_create=False,
+                type_update=False,
+                type_delete=False,
+                type_job_start=True,
+                type_job_end=False,
+                payload_url='http://example.com/?4',
+                enabled=False,
+                http_method='PATCH',
+                ssl_verification=False,
+            ),
+            Webhook(
+                name='Webhook 5',
+                type_create=False,
+                type_update=False,
+                type_delete=False,
+                type_job_start=False,
+                type_job_end=True,
+                payload_url='http://example.com/?5',
+                enabled=False,
+                http_method='PATCH',
+                ssl_verification=False,
+            ),
         )
         Webhook.objects.bulk_create(webhooks)
         webhooks[0].content_types.add(content_types[0])
         webhooks[1].content_types.add(content_types[1])
         webhooks[2].content_types.add(content_types[2])
+        webhooks[3].content_types.add(content_types[3])
+        webhooks[4].content_types.add(content_types[4])
 
     def test_name(self):
         params = {'name': ['Webhook 1', 'Webhook 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_content_types(self):
-        params = {'content_types': 'dcim.site'}
+        params = {'content_types': 'dcim.region'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-        params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
+        params = {'content_type_id': [ContentType.objects.get_for_model(Region).pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
     def test_type_create(self):
@@ -144,6 +182,14 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
         params = {'type_delete': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
+    def test_type_job_start(self):
+        params = {'type_job_start': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_type_job_end(self):
+        params = {'type_job_end': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_enabled(self):
         params = {'enabled': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 8 - 0
netbox/templates/extras/webhook.html

@@ -40,6 +40,14 @@
             <th scope="row">Delete</th>
             <td>{% checkmark object.type_delete %}</td>
           </tr>
+          <tr>
+            <th scope="row">Job start</th>
+            <td>{% checkmark object.type_job_start %}</td>
+          </tr>
+          <tr>
+            <th scope="row">Job end</th>
+            <td>{% checkmark object.type_job_end %}</td>
+          </tr>
         </table>
       </div>
     </div>