mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-18 13:38:16 -06:00
Misc cleanup
This commit is contained in:
parent
488f0c5427
commit
5b40220239
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# Events
|
||||
EVENT_CREATE = 'create'
|
||||
EVENT_UPDATE = 'update'
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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',
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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',)
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user