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:
committed by
Jeremy Stretch
parent
3260ae76f1
commit
697feed257
@ -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.
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
netbox/extras/migrations/0088_jobresult_webhooks.py
Normal file
38
netbox/extras/migrations/0088_jobresult_webhooks.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user