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

Closes #3451: Add pre-/post-change snapshots to webhooks

This commit is contained in:
Jeremy Stretch
2021-03-09 13:03:44 -05:00
parent c083b862a7
commit c6641ec1de
5 changed files with 70 additions and 25 deletions

View File

@ -38,7 +38,8 @@ The following data is available as context for Jinja2 templates:
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). * `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
* `username` - The name of the user account associated with the change. * `username` - The name of the user account associated with the change.
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. * `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API. * `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
### Default Request Body ### Default Request Body
@ -47,7 +48,7 @@ If no body template is specified, the request body will be populated with a JSON
```no-highlight ```no-highlight
{ {
"event": "created", "event": "created",
"timestamp": "2020-02-25 15:10:26.010582+00:00", "timestamp": "2021-03-09 17:55:33.968016+00:00",
"model": "site", "model": "site",
"username": "jstretch", "username": "jstretch",
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
@ -62,6 +63,17 @@ If no body template is specified, the request body will be populated with a JSON
}, },
"region": null, "region": null,
... ...
},
"snapshots": {
"prechange": null,
"postchange": {
"created": "2021-03-09",
"last_updated": "2021-03-09T17:55:33.851Z",
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
}
} }
} }
``` ```

View File

@ -13,6 +13,29 @@ later will be required.
Virtual interfaces can now be assigned to a "parent" physical interface, by setting the `parent` field on the Interface model. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 to the physical interface Gi0/0. Virtual interfaces can now be assigned to a "parent" physical interface, by setting the `parent` field on the Interface model. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 to the physical interface Gi0/0.
#### Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451))
In conjunction with the newly improved change logging functionality ([#5913](https://github.com/netbox-community/netbox/issues/5913)), outgoing webhooks now include a pre- and post-change representation of the modified object. These are available in the rendering context as a dictionary named `snapshots` with keys `prechange` and `postchange`. For example, here are the abridged snapshots resulting from renaming a site and changing its status:
```json
"snapshots": {
"prechange": {
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
},
"postchange": {
"name": "Site 2",
"slug": "site-2",
"status": "planned",
...
}
}
```
Note: The pre-change snapshot for an object creation will always be null, as will the post-change snapshot for an object deletion.
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648)) #### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where you don't necessarily know or care what is connected to the far end of a cable, but still need to designate the near end termination. Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where you don't necessarily know or care what is connected to the far end of a cable, but still need to designate the near end termination.

View File

@ -56,10 +56,10 @@ class WebhookTest(APITestCase):
# Verify that a job was queued for the object creation webhook # Verify that a job was queued for the object creation webhook
self.assertEqual(self.queue.count, 1) self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0] job = self.queue.jobs[0]
self.assertEqual(job.args[0], Webhook.objects.get(type_create=True)) self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_create=True))
self.assertEqual(job.args[1]['id'], response.data['id']) self.assertEqual(job.kwargs['data']['id'], response.data['id'])
self.assertEqual(job.args[2], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_CREATE) self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
def test_enqueue_webhook_update(self): def test_enqueue_webhook_update(self):
# Update an object via the REST API # Update an object via the REST API
@ -75,10 +75,10 @@ class WebhookTest(APITestCase):
# Verify that a job was queued for the object update webhook # Verify that a job was queued for the object update webhook
self.assertEqual(self.queue.count, 1) self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0] job = self.queue.jobs[0]
self.assertEqual(job.args[0], Webhook.objects.get(type_update=True)) self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_update=True))
self.assertEqual(job.args[1]['id'], site.pk) self.assertEqual(job.kwargs['data']['id'], site.pk)
self.assertEqual(job.args[2], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
def test_enqueue_webhook_delete(self): def test_enqueue_webhook_delete(self):
# Delete an object via the REST API # Delete an object via the REST API
@ -91,10 +91,10 @@ class WebhookTest(APITestCase):
# Verify that a job was queued for the object update webhook # Verify that a job was queued for the object update webhook
self.assertEqual(self.queue.count, 1) self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0] job = self.queue.jobs[0]
self.assertEqual(job.args[0], Webhook.objects.get(type_delete=True)) self.assertEqual(job.kwargs['webhook'], Webhook.objects.get(type_delete=True))
self.assertEqual(job.args[1]['id'], site.pk) self.assertEqual(job.kwargs['data']['id'], site.pk)
self.assertEqual(job.args[2], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.args[3], ObjectChangeActionChoices.ACTION_DELETE) self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
def test_webhooks_worker(self): def test_webhooks_worker(self):
@ -116,7 +116,7 @@ class WebhookTest(APITestCase):
# Validate the outgoing request body # Validate the outgoing request body
body = json.loads(request.body) body = json.loads(request.body)
self.assertEqual(body['event'], 'created') self.assertEqual(body['event'], 'created')
self.assertEqual(body['timestamp'], job.args[4]) self.assertEqual(body['timestamp'], job.kwargs['timestamp'])
self.assertEqual(body['model'], 'site') self.assertEqual(body['model'], 'site')
self.assertEqual(body['username'], 'testuser') self.assertEqual(body['username'], 'testuser')
self.assertEqual(body['request_id'], str(request_id)) self.assertEqual(body['request_id'], str(request_id))
@ -138,4 +138,4 @@ class WebhookTest(APITestCase):
# Patch the Session object with our dummy_send() method, then process the webhook for sending # Patch the Session object with our dummy_send() method, then process the webhook for sending
with patch.object(Session, 'send', dummy_send) as mock_send: with patch.object(Session, 'send', dummy_send) as mock_send:
process_webhook(*job.args) process_webhook(**job.kwargs)

View File

@ -6,6 +6,7 @@ from django.utils import timezone
from django_rq import get_queue from django_rq import get_queue
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import serialize_object
from .choices import * from .choices import *
from .models import Webhook from .models import Webhook
from .registry import registry from .registry import registry
@ -44,6 +45,7 @@ def enqueue_webhooks(instance, user, request_id, action):
webhooks = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True}) webhooks = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
if webhooks.exists(): if webhooks.exists():
# Get the Model's API serializer class and serialize the object # Get the Model's API serializer class and serialize the object
serializer_class = get_serializer_for_model(instance.__class__) serializer_class = get_serializer_for_model(instance.__class__)
serializer_context = { serializer_context = {
@ -51,16 +53,23 @@ def enqueue_webhooks(instance, user, request_id, action):
} }
serializer = serializer_class(instance, context=serializer_context) serializer = serializer_class(instance, context=serializer_context)
# Gather pre- and post-change snapshots
snapshots = {
'prechange': getattr(instance, '_prechange_snapshot', None),
'postchange': serialize_object(instance) if action != ObjectChangeActionChoices.ACTION_DELETE else None,
}
# Enqueue the webhooks # Enqueue the webhooks
webhook_queue = get_queue('default') webhook_queue = get_queue('default')
for webhook in webhooks: for webhook in webhooks:
webhook_queue.enqueue( webhook_queue.enqueue(
"extras.webhooks_worker.process_webhook", "extras.webhooks_worker.process_webhook",
webhook, webhook=webhook,
serializer.data, model_name=instance._meta.model_name,
instance._meta.model_name, event=action,
action, data=serializer.data,
str(timezone.now()), snapshots=snapshots,
user.username, timestamp=str(timezone.now()),
request_id username=user.username,
request_id=request_id
) )

View File

@ -12,7 +12,7 @@ logger = logging.getLogger('netbox.webhooks_worker')
@job('default') @job('default')
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id): def process_webhook(webhook, model_name, event, data, snapshots, timestamp, username, request_id):
""" """
Make a POST request to the defined Webhook Make a POST request to the defined Webhook
""" """
@ -22,7 +22,8 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
'model': model_name, 'model': model_name,
'username': username, 'username': username,
'request_id': request_id, 'request_id': request_id,
'data': data 'data': data,
'snapshots': snapshots,
} }
# Build the headers for the HTTP request # Build the headers for the HTTP request