Misc cleanup

This commit is contained in:
Jeremy Stretch 2023-11-20 10:11:10 -05:00
parent 488f0c5427
commit 5b40220239
20 changed files with 71 additions and 72 deletions

View File

@ -1,13 +1,13 @@
# Event Rules
NetBox includes the ability to execute certain functions in response in response to internal object changes. These include:
NetBox includes the ability to execute certain functions in response to internal object changes. These include:
* [Scripts](../customization/custom-scripts.md) execution
* [Webhooks](../integrations/webhooks.md) execution
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate and Event Rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
Each event must be associated with at least one NetBox object type and at least one event (create, update, or delete).
Each event must be associated with at least one NetBox object type and at least one event (e.g. create, update, or delete).
## Conditional Event Rules

View File

@ -1,6 +1,6 @@
# EventRule
An event rule is a mechanism for taking an action (such as running a script or sending a webhook) when a change takes place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating an event pointing to a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver.
An event rule is a mechanism for automatically taking an action (such as running a script or sending a webhook) in response to an event in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating an event for device objects and designating a webhook to be transmitted. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver.
See the [event rules documentation](../features/event-rules.md) for more information.
@ -12,15 +12,15 @@ A unique human-friendly name.
### Content Types
The type(s) of object in NetBox that will trigger the webhook.
The type(s) of object in NetBox that will trigger the rule.
### Enabled
If not selected, the webhook will be inactive.
If not selected, the event rule will not be processed.
### Events
The events which will trigger the action. At least one event type must be selected.
The events which will trigger the rule. At least one event type must be selected.
| Name | Description |
|------------|--------------------------------------|
@ -32,4 +32,4 @@ The events which will trigger the action. At least one event type must be select
### 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.
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, no action will be taken. An event rule that does not define any conditions will _always_ trigger.

View File

@ -170,7 +170,7 @@ class Job(models.Model):
self.save()
# Handle events
self.trigger_events(event=EVENT_JOB_START)
self.process_event(event=EVENT_JOB_START)
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
"""
@ -188,7 +188,7 @@ class Job(models.Model):
self.save()
# Handle events
self.trigger_events(event=EVENT_JOB_END)
self.process_event(event=EVENT_JOB_END)
@classmethod
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
@ -225,10 +225,13 @@ class Job(models.Model):
return job
def trigger_events(self, event):
def process_event(self, event):
"""
Process any EventRules relevant to the passed job event (i.e. start or stop).
"""
from extras.models import EventRule
# Fetch any webhooks matching this object type and action
# Fetch any event rules matching this object type and action
event_rules = EventRule.objects.filter(
**{f'type_{event}': True},
content_types=self.object_type,

View File

@ -72,8 +72,8 @@ class EventRuleSerializer(NetBoxModelSerializer):
model = EventRule
fields = [
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type',
'custom_fields', 'tags', 'created', 'last_updated',
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'custom_fields', 'tags',
'created', 'last_updated',
]
@ -87,10 +87,9 @@ class WebhookSerializer(NetBoxModelSerializer):
class Meta:
model = Webhook
fields = [
'id', 'url', 'display', 'name',
'payload_url', 'http_method', 'http_content_type',
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path',
'custom_fields', 'tags', 'created', 'last_updated',
'id', 'url', 'display', 'name', 'payload_url', 'http_method', 'http_content_type', 'additional_headers',
'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', 'tags', 'created',
'last_updated',
]

View File

@ -1,6 +1,3 @@
from django.db.models import Q
# Events
EVENT_CREATE = 'create'
EVENT_UPDATE = 'update'

View File

@ -7,8 +7,8 @@ from .events import flush_events
@contextmanager
def event_tracking(request):
"""
Enable event tracking by connecting the appropriate signals to their receivers before code is run, and
disconnecting them afterward.
Queue interesting events in memory while processing a request, then flush that queue for processing by the
events pipline before returning the response.
:param request: WSGIRequest object with a unique `id` set
"""

View File

@ -3,11 +3,7 @@ import sys
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django_rq import get_queue
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.registry import registry
from utilities.api import get_serializer_for_model
from utilities.utils import serialize_object
@ -68,7 +64,7 @@ def enqueue_object(queue, instance, user, request_id, action):
})
def process_event_queue(queue):
def process_event_queue(events):
"""
Flush a list of object representation to RQ for EventRule processing.
"""
@ -78,7 +74,7 @@ def process_event_queue(queue):
'type_delete': {},
}
for data in queue:
for data in events:
action_flag = {
ObjectChangeActionChoices.ACTION_CREATE: 'type_create',
ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',

View File

@ -73,7 +73,8 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
class Meta:
model = EventRule
fields = [
'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'description',
'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
'description',
]
def search(self, queryset, name, value):

View File

@ -197,7 +197,7 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm):
label=_('CA file path')
)
nullable_fields = ('secret', 'conditions', 'ca_file_path')
nullable_fields = ('secret', 'ca_file_path')
class EventRuleBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -156,8 +156,8 @@ class EventRuleImportForm(NetBoxModelImportForm):
class Meta:
model = EventRule
fields = (
'name', 'description', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'type_job_start',
'type_job_end', 'comments', 'tags'
'name', 'description', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end', 'comments', 'tags'
)

View File

@ -22,7 +22,6 @@ from utilities.forms.fields import (
from utilities.forms.widgets import ChoicesWidget, HTMXSelect
from virtualization.models import Cluster, ClusterGroup, ClusterType
__all__ = (
'BookmarkForm',
'ConfigContextForm',
@ -257,12 +256,19 @@ class EventRuleForm(NetBoxModelForm):
(_('EventRule'), ('name', 'description', 'content_types', 'enabled', 'tags')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
(_('Conditions'), ('conditions',)),
(_('Action'), ('action_type', 'action_choice', 'action_parameters', 'action_object_type', 'action_object_id', 'action_data')),
(_('Action'), (
'action_type', 'action_choice', 'action_parameters', 'action_object_type', 'action_object_id',
'action_data',
)),
)
class Meta:
model = EventRule
fields = '__all__'
fields = (
'content_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start',
'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id',
'action_parameters', 'action_data', 'comments', 'tags'
)
labels = {
'type_create': _('Creations'),
'type_update': _('Updates'),
@ -280,7 +286,6 @@ class EventRuleForm(NetBoxModelForm):
def get_script_choices(self):
choices = []
idx = 0
for module in ScriptModule.objects.all():
scripts = []
for script_name in module.scripts.keys():

View File

@ -49,7 +49,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
to='contenttypes.ContentType',
related_name='eventrules',
verbose_name=_('object types'),
help_text=_("The object(s) to which this Event applies.")
help_text=_("The object(s) to which this rule applies.")
)
name = models.CharField(
verbose_name=_('name'),
@ -169,11 +169,6 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
Each Webhook can be limited to firing only on certain actions or certain object types.
"""
events = GenericRelation(
EventRule,
content_type_field='action_object_type',
object_id_field='action_object_id'
)
name = models.CharField(
verbose_name=_('name'),
max_length=150,
@ -243,6 +238,11 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
"The specific CA certificate file to use for SSL verification. Leave blank to use the system defaults."
)
)
events = GenericRelation(
EventRule,
content_type_field='action_object_type',
object_id_field='action_object_id'
)
class Meta:
ordering = ('name',)

View File

@ -476,6 +476,12 @@ def run_script(data, job, request=None, commit=True, **kwargs):
"""
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
exists outside the Script class to ensure it cannot be overridden by a script author.
Args:
data: A dictionary of data to be passed to the script upon execution
job: The Job associated with this execution
request: The WSGI request associated with this execution (if any)
commit: Passed through to Script.run()
"""
job.start()
@ -507,6 +513,7 @@ def run_script(data, job, request=None, commit=True, **kwargs):
raise AbortTransaction()
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
if request:
clear_webhooks.send(request)
job.data = ScriptOutputSerializer(script).data
job.terminate()
@ -521,12 +528,13 @@ def run_script(data, job, request=None, commit=True, **kwargs):
script.log_info("Database changes have been reverted due to error.")
job.data = ScriptOutputSerializer(script).data
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=str(e))
if request:
clear_webhooks.send(request)
logger.info(f"Script completed in {job.duration}")
# Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
# change logging, webhooks, etc.
# change logging, event rules, etc.
if commit:
with event_tracking(request):
_run_script()

View File

@ -1,21 +1,15 @@
import logging
import requests
from core.models import Job
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django_rq import job
from jinja2.exceptions import TemplateError
from utilities.rqworker import get_workers_for_queue
from extras.constants import WEBHOOK_EVENT_TYPES
from core.models import Job
from extras.models import ScriptModule
from extras.scripts import run_script
from extras.utils import eval_conditions
from extras.webhooks import generate_signature
logger = logging.getLogger('netbox.webhooks_worker')
logger = logging.getLogger('netbox.scripts_worker')
@job('default')
@ -43,7 +37,7 @@ def process_script(event_rule, model_name, event, data, timestamp, username, req
script = module.scripts[script_name]()
job = Job.enqueue(
Job.enqueue(
run_script,
instance=module,
name=script.class_name,

View File

@ -14,8 +14,8 @@ from netbox.context import current_request, events_queue
from netbox.signals import post_clean
from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
from .events import enqueue_object, get_snapshots, serialize_for_event
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
#
# Change logging/webhooks

View File

@ -284,8 +284,8 @@ class WebhookTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Webhook
fields = (
'pk', 'id', 'name', 'http_method', 'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'http_method', 'payload_url', 'http_content_type', 'secret', 'ssl_verification',
'ca_file_path', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'http_method', 'payload_url',
@ -328,12 +328,12 @@ class EventRuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = EventRule
fields = (
'pk', 'id', 'name', 'action_type', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'content_types', 'type_create', 'type_update',
'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'action_type', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'type_job_start',
'type_job_end',
'pk', 'name', 'enabled', 'action_type', 'content_types', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end',
)

View File

@ -106,7 +106,7 @@ def process_event_rules(event_rules, model_name, event, data, username, snapshot
elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
processor = "extras.scripts_worker.process_script"
else:
raise ValueError(f"Unknown Event Rule action type: {event_rule.action_type}")
raise ValueError(f"Unknown action type for an event rule: {event_rule.action_type}")
params = {
"event_rule": event_rule,

View File

@ -178,7 +178,6 @@ TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False)
# Check for hard-coded dynamic config parameters
for param in PARAMS:
if hasattr(configuration, param.name):

View File

@ -101,11 +101,9 @@ class JSONField(_JSONField):
self.widget.attrs['class'] = 'font-monospace'
def prepare_value(self, value):
if value == '':
return value
if isinstance(value, InvalidJSONInput):
return value
if value is None:
if value in ('', None):
return ''
return json.dumps(value, sort_keys=True, indent=4)

View File

@ -128,8 +128,7 @@ def get_field_value(form, field_name):
"""
field = form.fields[field_name]
if form.is_bound:
if data := form.data.get(field_name):
if form.is_bound and (data := form.data.get(field_name)):
if hasattr(field, 'valid_value') and field.valid_value(data):
return data