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

Closes #5610: Add REST API endpoint for webhooks

This commit is contained in:
Jeremy Stretch
2021-03-09 09:22:58 -05:00
parent 38ded66c4e
commit 6ffadb501b
9 changed files with 233 additions and 11 deletions

View File

@ -44,6 +44,7 @@ The ObjectChange model (which is used to record the creation, modification, and
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models * [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields * [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links * [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI * [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location * [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models * [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
@ -89,3 +90,5 @@ The ObjectChange model (which is used to record the creation, modification, and
* extras.ObjectChange * extras.ObjectChange
* Added the `prechange_data` field * Added the `prechange_data` field
* Renamed `object_data` to `postchange_data` * Renamed `object_data` to `postchange_data`
* extras.Webhook
* Added the `/api/extras/webhooks/` endpoint

View File

@ -12,9 +12,18 @@ __all__ = [
'NestedImageAttachmentSerializer', 'NestedImageAttachmentSerializer',
'NestedJobResultSerializer', 'NestedJobResultSerializer',
'NestedTagSerializer', 'NestedTagSerializer',
'NestedWebhookSerializer',
] ]
class NestedWebhookSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
class Meta:
model = models.Webhook
fields = ['id', 'url', 'name']
class NestedCustomFieldSerializer(WritableNestedSerializer): class NestedCustomFieldSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')

View File

@ -9,9 +9,7 @@ from dcim.api.nested_serializers import (
) )
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import ( from extras.models import *
ConfigContext, CustomField, CustomLink, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag,
)
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, ValidatedModelSerializer
from netbox.api.exceptions import SerializerNotFound from netbox.api.exceptions import SerializerNotFound
@ -24,6 +22,48 @@ from virtualization.models import Cluster, ClusterGroup
from .nested_serializers import * from .nested_serializers import *
__all__ = (
'ConfigContextSerializer',
'ContentTypeSerializer',
'CustomFieldSerializer',
'CustomLinkSerializer',
'ExportTemplateSerializer',
'ImageAttachmentSerializer',
'JobResultSerializer',
'ObjectChangeSerializer',
'ReportDetailSerializer',
'ReportSerializer',
'ScriptDetailSerializer',
'ScriptInputSerializer',
'ScriptLogMessageSerializer',
'ScriptOutputSerializer',
'ScriptSerializer',
'TagSerializer',
'TaggedObjectSerializer',
'WebhookSerializer',
)
#
# Webhooks
#
class WebhookSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
many=True
)
class Meta:
model = Webhook
fields = [
'id', 'url', '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',
]
# #
# Custom fields # Custom fields
# #

View File

@ -5,6 +5,9 @@ from . import views
router = OrderedDefaultRouter() router = OrderedDefaultRouter()
router.APIRootView = views.ExtrasRootView router.APIRootView = views.ExtrasRootView
# Webhooks
router.register('webhooks', views.WebhookViewSet)
# Custom fields # Custom fields
router.register('custom-fields', views.CustomFieldViewSet) router.register('custom-fields', views.CustomFieldViewSet)

View File

@ -11,9 +11,7 @@ from rq import Worker
from extras import filters from extras import filters
from extras.choices import JobResultStatusChoices from extras.choices import JobResultStatusChoices
from extras.models import ( from extras.models import *
ConfigContext, CustomLink, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem,
)
from extras.models import CustomField from extras.models import CustomField
from extras.reports import get_report, get_reports, run_report from extras.reports import get_report, get_reports, run_report
from extras.scripts import get_script, get_scripts, run_script from extras.scripts import get_script, get_scripts, run_script
@ -55,6 +53,17 @@ class ConfigContextQuerySetMixin:
return queryset.annotate_config_context_data() return queryset.annotate_config_context_data()
#
# Webhooks
#
class WebhookViewSet(ModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Webhook.objects.all()
serializer_class = serializers.WebhookSerializer
filterset_class = filters.WebhookFilterSet
# #
# Custom fields # Custom fields
# #

View File

@ -9,9 +9,7 @@ from tenancy.models import Tenant, TenantGroup
from utilities.filters import BaseFilterSet, ContentTypeFilter from utilities.filters import BaseFilterSet, ContentTypeFilter
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
from .choices import * from .choices import *
from .models import ( from .models import *
ConfigContext, CustomField, CustomLink, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag,
)
__all__ = ( __all__ = (
@ -26,6 +24,7 @@ __all__ = (
'LocalConfigContextFilterSet', 'LocalConfigContextFilterSet',
'ObjectChangeFilterSet', 'ObjectChangeFilterSet',
'TagFilterSet', 'TagFilterSet',
'WebhookFilterSet',
) )
EXACT_FILTER_TYPES = ( EXACT_FILTER_TYPES = (
@ -36,6 +35,20 @@ EXACT_FILTER_TYPES = (
) )
class WebhookFilterSet(BaseFilterSet):
content_types = ContentTypeFilter()
http_method = django_filters.MultipleChoiceFilter(
choices=WebhookHttpMethodChoices
)
class Meta:
model = Webhook
fields = [
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
]
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):
""" """
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name. Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.

View File

@ -21,6 +21,19 @@ from utilities.querysets import RestrictedQuerySet
from utilities.utils import deepmerge, render_jinja2 from utilities.utils import deepmerge, render_jinja2
__all__ = (
'ConfigContext',
'ConfigContextModel',
'CustomLink',
'ExportTemplate',
'ImageAttachment',
'JobResult',
'Report',
'Script',
'Webhook',
)
# #
# Webhooks # Webhooks
# #
@ -109,6 +122,8 @@ class Webhook(BigIDModel):
'Leave blank to use the system defaults.' 'Leave blank to use the system defaults.'
) )
objects = RestrictedQuerySet.as_manager()
class Meta: class Meta:
ordering = ('name',) ordering = ('name',)
unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',) unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',)

View File

@ -11,7 +11,7 @@ from rq import Worker
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.api.views import ReportViewSet, ScriptViewSet from extras.api.views import ReportViewSet, ScriptViewSet
from extras.models import ConfigContext, CustomField, CustomLink, ExportTemplate, ImageAttachment, Tag from extras.models import *
from extras.reports import Report from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -30,6 +30,60 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class WebhookTest(APIViewTestCases.APIViewTestCase):
model = Webhook
brief_fields = ['id', 'name', 'url']
create_data = [
{
'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 4',
'type_create': True,
'payload_url': 'http://example.com/?4',
},
{
'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 5',
'type_update': True,
'payload_url': 'http://example.com/?5',
},
{
'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 6',
'type_delete': True,
'payload_url': 'http://example.com/?6',
},
]
bulk_update_data = {
'ssl_verification': False,
}
@classmethod
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
rack_ct = ContentType.objects.get_for_model(Rack)
webhooks = (
Webhook(
name='Webhook 1',
type_create=True,
payload_url='http://example.com/?1',
),
Webhook(
name='Webhook 2',
type_update=True,
payload_url='http://example.com/?1',
),
Webhook(
name='Webhook 3',
type_delete=True,
payload_url='http://example.com/?1',
),
)
Webhook.objects.bulk_create(webhooks)
for webhook in webhooks:
webhook.content_types.add(site_ct, rack_ct)
class CustomFieldTest(APIViewTestCases.APIViewTestCase): class CustomFieldTest(APIViewTestCases.APIViewTestCase):
model = CustomField model = CustomField
brief_fields = ['id', 'name', 'url'] brief_fields = ['id', 'name', 'url']

View File

@ -7,12 +7,88 @@ from django.test import TestCase
from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup
from extras.choices import ObjectChangeActionChoices from extras.choices import ObjectChangeActionChoices
from extras.filters import * from extras.filters import *
from extras.models import ConfigContext, CustomLink, ExportTemplate, ImageAttachment, ObjectChange, Tag from extras.models import *
from ipam.models import IPAddress from ipam.models import IPAddress
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
class WebhookTestCase(TestCase):
queryset = Webhook.objects.all()
filterset = WebhookFilterSet
@classmethod
def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
webhooks = (
Webhook(
name='Webhook 1',
type_create=True,
payload_url='http://example.com/?1',
enabled=True,
http_method='GET',
ssl_verification=True,
),
Webhook(
name='Webhook 2',
type_update=True,
payload_url='http://example.com/?2',
enabled=True,
http_method='POST',
ssl_verification=True,
),
Webhook(
name='Webhook 3',
type_delete=True,
payload_url='http://example.com/?3',
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])
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
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'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_create(self):
params = {'type_create': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_update(self):
params = {'type_update': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_delete(self):
params = {'type_delete': 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)
def test_http_method(self):
params = {'http_method': ['GET', 'POST']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ssl_verification(self):
params = {'ssl_verification': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CustomLinkTestCase(TestCase): class CustomLinkTestCase(TestCase):
queryset = CustomLink.objects.all() queryset = CustomLink.objects.all()
filterset = CustomLinkFilterSet filterset = CustomLinkFilterSet