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 # 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 * [Scripts](../customization/custom-scripts.md) execution
* [Webhooks](../integrations/webhooks.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 ## Conditional Event Rules

View File

@ -1,6 +1,6 @@
# EventRule # 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. See the [event rules documentation](../features/event-rules.md) for more information.
@ -12,15 +12,15 @@ A unique human-friendly name.
### Content Types ### 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 ### Enabled
If not selected, the webhook will be inactive. If not selected, the event rule will not be processed.
### Events ### 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 | | Name | Description |
|------------|--------------------------------------| |------------|--------------------------------------|
@ -32,4 +32,4 @@ The events which will trigger the action. At least one event type must be select
### Conditions ### 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() self.save()
# Handle events # 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): def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
""" """
@ -188,7 +188,7 @@ class Job(models.Model):
self.save() self.save()
# Handle events # Handle events
self.trigger_events(event=EVENT_JOB_END) self.process_event(event=EVENT_JOB_END)
@classmethod @classmethod
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs): def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
@ -225,10 +225,13 @@ class Job(models.Model):
return job 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 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( event_rules = EventRule.objects.filter(
**{f'type_{event}': True}, **{f'type_{event}': True},
content_types=self.object_type, content_types=self.object_type,

View File

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

View File

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

View File

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

View File

@ -3,11 +3,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType 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 netbox.registry import registry
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import serialize_object 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. Flush a list of object representation to RQ for EventRule processing.
""" """
@ -78,7 +74,7 @@ def process_event_queue(queue):
'type_delete': {}, 'type_delete': {},
} }
for data in queue: for data in events:
action_flag = { action_flag = {
ObjectChangeActionChoices.ACTION_CREATE: 'type_create', ObjectChangeActionChoices.ACTION_CREATE: 'type_create',
ObjectChangeActionChoices.ACTION_UPDATE: 'type_update', ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',

View File

@ -73,7 +73,8 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = EventRule model = EventRule
fields = [ 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): def search(self, queryset, name, value):

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
to='contenttypes.ContentType', to='contenttypes.ContentType',
related_name='eventrules', related_name='eventrules',
verbose_name=_('object types'), 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( name = models.CharField(
verbose_name=_('name'), 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. 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. 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( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=150, 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." "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: class Meta:
ordering = ('name',) 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 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. 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() job.start()
@ -507,7 +513,8 @@ def run_script(data, job, request=None, commit=True, **kwargs):
raise AbortTransaction() raise AbortTransaction()
except AbortTransaction: except AbortTransaction:
script.log_info("Database changes have been reverted automatically.") script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request) if request:
clear_webhooks.send(request)
job.data = ScriptOutputSerializer(script).data job.data = ScriptOutputSerializer(script).data
job.terminate() job.terminate()
except Exception as e: except Exception as e:
@ -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.") script.log_info("Database changes have been reverted due to error.")
job.data = ScriptOutputSerializer(script).data job.data = ScriptOutputSerializer(script).data
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=str(e)) job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=str(e))
clear_webhooks.send(request) if request:
clear_webhooks.send(request)
logger.info(f"Script completed in {job.duration}") 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 # 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: if commit:
with event_tracking(request): with event_tracking(request):
_run_script() _run_script()

View File

@ -1,21 +1,15 @@
import logging 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.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django_rq import job 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.models import ScriptModule
from extras.scripts import run_script from extras.scripts import run_script
from extras.utils import eval_conditions 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') @job('default')
@ -43,7 +37,7 @@ def process_script(event_rule, model_name, event, data, timestamp, username, req
script = module.scripts[script_name]() script = module.scripts[script_name]()
job = Job.enqueue( Job.enqueue(
run_script, run_script,
instance=module, instance=module,
name=script.class_name, 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 netbox.signals import post_clean
from utilities.exceptions import AbortRequest from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
from .events import enqueue_object, get_snapshots, serialize_for_event from .events import enqueue_object, get_snapshots, serialize_for_event
from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
# #
# Change logging/webhooks # Change logging/webhooks

View File

@ -284,8 +284,8 @@ class WebhookTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Webhook model = Webhook
fields = ( fields = (
'pk', 'id', 'name', 'http_method', 'payload_url', 'secret', 'ssl_validation', 'ca_file_path', 'pk', 'id', 'name', 'http_method', 'payload_url', 'http_content_type', 'secret', 'ssl_verification',
'tags', 'created', 'last_updated', 'ca_file_path', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'http_method', 'payload_url', 'pk', 'name', 'http_method', 'payload_url',
@ -328,12 +328,12 @@ class EventRuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = EventRule model = EventRule
fields = ( fields = (
'pk', 'id', 'name', 'action_type', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'content_types', 'type_create', 'type_update',
'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'action_type', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'pk', 'name', 'enabled', 'action_type', 'content_types', 'type_create', 'type_update', 'type_delete',
'type_job_end', '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: elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
processor = "extras.scripts_worker.process_script" processor = "extras.scripts_worker.process_script"
else: 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 = { params = {
"event_rule": event_rule, "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') TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False) ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False)
# Check for hard-coded dynamic config parameters # Check for hard-coded dynamic config parameters
for param in PARAMS: for param in PARAMS:
if hasattr(configuration, param.name): if hasattr(configuration, param.name):

View File

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

View File

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