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

@ -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.

View File

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

View File

@ -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):

View File

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

View File

@ -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'
)

View File

@ -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')
)

View File

@ -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'}),

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.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=_("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(
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:

View File

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

View File

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

View File

@ -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>