1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Add type_job_start & type_job_end to Webhook

This commit is contained in:
jeremystretch
2023-02-28 14:29:00 -05:00
committed by Jeremy Stretch
parent 3260ae76f1
commit 697feed257
12 changed files with 177 additions and 33 deletions

View File

@ -23,10 +23,12 @@ If not selected, the webhook will be inactive.
The events which will trigger the webhook. At least one event type must be selected. The events which will trigger the webhook. At least one event type must be selected.
| Name | Description | | Name | Description |
|-----------|--------------------------------------| |------------|--------------------------------------|
| Creations | A new object has been created | | Creations | A new object has been created |
| Updates | An existing object has been modified | | Updates | An existing object has been modified |
| Deletions | An object has been deleted | | Deletions | An object has been deleted |
| Job starts | A job for an object starts |
| Job ends | A job for an object terminates |
### URL ### 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. 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 ### SSL Verification
Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used. Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used.

View File

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

View File

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

View File

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

View File

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

View File

@ -222,7 +222,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'http_method', 'enabled')), ('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( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()), queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
@ -244,19 +244,36 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) ),
label=_('Object creations')
) )
type_update = forms.NullBooleanField( type_update = forms.NullBooleanField(
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) ),
label=_('Object updates')
) )
type_delete = forms.NullBooleanField( type_delete = forms.NullBooleanField(
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES 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')
) )

View File

@ -154,7 +154,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
fieldsets = ( fieldsets = (
('Webhook', ('name', 'content_types', 'enabled')), ('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', ( ('HTTP Request', (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', '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_create': 'Creations',
'type_update': 'Updates', 'type_update': 'Updates',
'type_delete': 'Deletions', 'type_delete': 'Deletions',
'type_job_start': 'Job executions',
'type_job_end': 'Job terminations',
} }
widgets = { widgets = {
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),

View File

@ -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),
),
]

View File

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

View File

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

View File

@ -89,12 +89,16 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): 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 = ( webhooks = (
Webhook( Webhook(
name='Webhook 1', name='Webhook 1',
type_create=True, type_create=True,
type_update=False,
type_delete=False,
type_job_start=False,
type_job_end=False,
payload_url='http://example.com/?1', payload_url='http://example.com/?1',
enabled=True, enabled=True,
http_method='GET', http_method='GET',
@ -102,7 +106,11 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
), ),
Webhook( Webhook(
name='Webhook 2', name='Webhook 2',
type_create=False,
type_update=True, type_update=True,
type_delete=False,
type_job_start=False,
type_job_end=False,
payload_url='http://example.com/?2', payload_url='http://example.com/?2',
enabled=True, enabled=True,
http_method='POST', http_method='POST',
@ -110,26 +118,56 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
), ),
Webhook( Webhook(
name='Webhook 3', name='Webhook 3',
type_create=False,
type_update=False,
type_delete=True, type_delete=True,
type_job_start=False,
type_job_end=False,
payload_url='http://example.com/?3', payload_url='http://example.com/?3',
enabled=False, enabled=False,
http_method='PATCH', http_method='PATCH',
ssl_verification=False, 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) Webhook.objects.bulk_create(webhooks)
webhooks[0].content_types.add(content_types[0]) webhooks[0].content_types.add(content_types[0])
webhooks[1].content_types.add(content_types[1]) webhooks[1].content_types.add(content_types[1])
webhooks[2].content_types.add(content_types[2]) 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): def test_name(self):
params = {'name': ['Webhook 1', 'Webhook 2']} params = {'name': ['Webhook 1', 'Webhook 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): 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) 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) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_create(self): def test_type_create(self):
@ -144,6 +182,14 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
params = {'type_delete': True} params = {'type_delete': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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): def test_enabled(self):
params = {'enabled': True} params = {'enabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -40,6 +40,14 @@
<th scope="row">Delete</th> <th scope="row">Delete</th>
<td>{% checkmark object.type_delete %}</td> <td>{% checkmark object.type_delete %}</td>
</tr> </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> </table>
</div> </div>
</div> </div>