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:
@ -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",
|
||||||
|
...
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user