Initial work on #16886

This commit is contained in:
Jeremy Stretch 2024-07-17 21:02:12 -04:00
parent b2c5a4639c
commit 8e2f459d10
23 changed files with 238 additions and 245 deletions

View File

@ -0,0 +1,3 @@
# Events
TODO

View File

@ -142,6 +142,7 @@ nav:
- Forms: 'plugins/development/forms.md' - Forms: 'plugins/development/forms.md'
- Filters & Filter Sets: 'plugins/development/filtersets.md' - Filters & Filter Sets: 'plugins/development/filtersets.md'
- Search: 'plugins/development/search.md' - Search: 'plugins/development/search.md'
- Events: 'plugins/development/events.md'
- Data Backends: 'plugins/development/data-backends.md' - Data Backends: 'plugins/development/data-backends.md'
- REST API: 'plugins/development/rest-api.md' - REST API: 'plugins/development/rest-api.md'
- GraphQL API: 'plugins/development/graphql-api.md' - GraphQL API: 'plugins/development/graphql-api.md'

View File

@ -12,16 +12,18 @@ __all__ = (
'OBJECT_UPDATED', 'OBJECT_UPDATED',
) )
NAMESPACE = 'netbox'
# Object events # Object events
OBJECT_CREATED = 'object_created' OBJECT_CREATED = f'{NAMESPACE}.object_created'
OBJECT_UPDATED = 'object_updated' OBJECT_UPDATED = f'{NAMESPACE}.object_updated'
OBJECT_DELETED = 'object_deleted' OBJECT_DELETED = f'{NAMESPACE}.object_deleted'
# Job events # Job events
JOB_STARTED = 'job_started' JOB_STARTED = f'{NAMESPACE}.job_started'
JOB_COMPLETED = 'job_completed' JOB_COMPLETED = f'{NAMESPACE}.job_completed'
JOB_FAILED = 'job_failed' JOB_FAILED = f'{NAMESPACE}.job_failed'
JOB_ERRORED = 'job_errored' JOB_ERRORED = f'{NAMESPACE}.job_errored'
# Register core events # Register core events
Event(name=OBJECT_CREATED, text=_('Object created')).register() Event(name=OBJECT_CREATED, text=_('Object created')).register()

View File

@ -34,9 +34,9 @@ class EventRuleSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = EventRule model = EventRule
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete', 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'event_types', 'conditions',
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_type', 'action_object_type', 'action_object_id', 'action_object', 'description', 'custom_fields',
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'name', 'description') brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -176,7 +176,7 @@ def process_event_queue(events):
# Cache applicable Event Rules # Cache applicable Event Rules
if object_type not in events_cache[action_flag]: if object_type not in events_cache[action_flag]:
events_cache[action_flag][object_type] = EventRule.objects.filter( events_cache[action_flag][object_type] = EventRule.objects.filter(
**{action_flag: True}, event_types__contains=[event['event_type']],
object_types=object_type, object_types=object_type,
enabled=True enabled=True
) )

View File

@ -99,6 +99,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
object_type = ContentTypeFilter( object_type = ContentTypeFilter(
field_name='object_types' field_name='object_types'
) )
event_type = MultiValueCharFilter(
method='filter_event_type'
)
action_type = django_filters.MultipleChoiceFilter( action_type = django_filters.MultipleChoiceFilter(
choices=EventRuleActionChoices choices=EventRuleActionChoices
) )
@ -108,8 +111,7 @@ 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', 'id', 'name', 'enabled', 'action_type', 'description',
'action_type', 'description',
) )
def search(self, queryset, name, value): def search(self, queryset, name, value):
@ -121,6 +123,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
Q(comments__icontains=value) Q(comments__icontains=value)
) )
def filter_event_type(self, queryset, name, value):
return queryset.filter(event_types__overlap=value)
class CustomFieldFilterSet(ChangeLoggedModelFilterSet): class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.choices import * from extras.choices import *
@ -248,33 +249,18 @@ class EventRuleBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_create = forms.NullBooleanField( event_types = SimpleArrayField(
label=_('On create'), label=_('Event types'),
required=False, base_field=forms.CharField(),
widget=BulkEditNullBooleanSelect() required=False
) )
type_update = forms.NullBooleanField( description = forms.CharField(
label=_('On update'), label=_('Description'),
required=False, max_length=200,
widget=BulkEditNullBooleanSelect() required=False
)
type_delete = forms.NullBooleanField(
label=_('On delete'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_job_start = forms.NullBooleanField(
label=_('On job start'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_job_end = forms.NullBooleanField(
label=_('On job end'),
required=False,
widget=BulkEditNullBooleanSelect()
) )
nullable_fields = ('description', 'conditions',) nullable_fields = ('description', 'conditions')
class TagBulkEditForm(BulkEditForm): class TagBulkEditForm(BulkEditForm):

View File

@ -196,8 +196,8 @@ class EventRuleImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = EventRule model = EventRule
fields = ( fields = (
'name', 'description', 'enabled', 'conditions', 'object_types', 'type_create', 'type_update', 'name', 'description', 'enabled', 'conditions', 'object_types', 'event_types', 'action_type',
'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags' 'action_object', 'comments', 'tags'
) )
def clean(self): def clean(self):

View File

@ -6,6 +6,7 @@ from core.models import ObjectType, DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from netbox.events import get_event_type_choices
from netbox.forms.base import NetBoxModelFilterSetForm from netbox.forms.base import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin from netbox.forms.mixins import SavedFiltersMixin
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@ -274,14 +275,18 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('object_type_id', 'action_type', 'enabled', name=_('Attributes')), FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
) )
object_type_id = ContentTypeMultipleChoiceField( object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
required=False, required=False,
label=_('Object type') label=_('Object type')
) )
event_type = forms.MultipleChoiceField(
choices=get_event_type_choices,
required=False,
label=_('Event type')
)
action_type = forms.ChoiceField( action_type = forms.ChoiceField(
choices=add_blank_choice(EventRuleActionChoices), choices=add_blank_choice(EventRuleActionChoices),
required=False, required=False,
@ -294,41 +299,6 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
type_create = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
label=_('Object creations')
)
type_update = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
label=_('Object updates')
)
type_delete = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
label=_('Object deletions')
)
type_job_start = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
label=_('Job starts')
)
type_job_end = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
),
label=_('Job terminations')
)
class TagFilterForm(SavedFiltersMixin, FilterForm): class TagFilterForm(SavedFiltersMixin, FilterForm):

View File

@ -2,6 +2,7 @@ import json
import re import re
from django import forms from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -303,6 +304,10 @@ class EventRuleForm(NetBoxModelForm):
label=_('Object types'), label=_('Object types'),
queryset=ObjectType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
) )
event_types = SimpleArrayField(
label=_('Event types'),
base_field=forms.CharField()
)
action_choice = forms.ChoiceField( action_choice = forms.ChoiceField(
label=_('Action choice'), label=_('Action choice'),
choices=[] choices=[]
@ -319,7 +324,7 @@ class EventRuleForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')), FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')), FieldSet('event_types', name=_('Event Types')),
FieldSet('conditions', name=_('Conditions')), FieldSet('conditions', name=_('Conditions')),
FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')), FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
) )
@ -327,17 +332,9 @@ class EventRuleForm(NetBoxModelForm):
class Meta: class Meta:
model = EventRule model = EventRule
fields = ( fields = (
'object_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags'
'action_data', 'comments', 'tags'
) )
labels = {
'type_create': _('Creations'),
'type_update': _('Updates'),
'type_delete': _('Deletions'),
'type_job_start': _('Job executions'),
'type_job_end': _('Job terminations'),
}
widgets = { widgets = {
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
'action_type': HTMXSelect(), 'action_type': HTMXSelect(),

View File

@ -9,7 +9,7 @@ __all__ = (
'CustomFieldFilter', 'CustomFieldFilter',
'CustomFieldChoiceSetFilter', 'CustomFieldChoiceSetFilter',
'CustomLinkFilter', 'CustomLinkFilter',
'EventRuleFilter', # 'EventRuleFilter',
'ExportTemplateFilter', 'ExportTemplateFilter',
'ImageAttachmentFilter', 'ImageAttachmentFilter',
'JournalEntryFilter', 'JournalEntryFilter',
@ -92,7 +92,7 @@ class WebhookFilter(BaseFilterMixin):
pass pass
@strawberry_django.filter(models.EventRule, lookups=True) # @strawberry_django.filter(models.EventRule, lookups=True)
@autotype_decorator(filtersets.EventRuleFilterSet) # @autotype_decorator(filtersets.EventRuleFilterSet)
class EventRuleFilter(BaseFilterMixin): # class EventRuleFilter(BaseFilterMixin):
pass # pass

View File

@ -182,7 +182,8 @@ class WebhookType(OrganizationalObjectType):
@strawberry_django.type( @strawberry_django.type(
models.EventRule, models.EventRule,
exclude=['content_types',], exclude=['content_types',],
filters=EventRuleFilter # TODO: Fix GraphQL filter
# filters=EventRuleFilter
) )
class EventRuleType(OrganizationalObjectType): class EventRuleType(OrganizationalObjectType):
action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None action_object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None

View File

@ -0,0 +1,75 @@
import django.contrib.postgres.fields
from django.db import migrations, models
from core.events import *
def set_event_types(apps, schema_editor):
EventRule = apps.get_model('extras', 'EventRule')
event_rules = EventRule.objects.all()
for event_rule in event_rules:
event_rule.event_types = []
if event_rule.type_create:
event_rule.event_types.append(OBJECT_CREATED)
if event_rule.type_update:
event_rule.event_types.append(OBJECT_UPDATED)
if event_rule.type_delete:
event_rule.event_types.append(OBJECT_DELETED)
if event_rule.type_job_start:
event_rule.event_types.append(JOB_STARTED)
if event_rule.type_job_end:
# Map type_job_end to all job termination events
event_rule.event_types.extend([JOB_COMPLETED, JOB_ERRORED, JOB_FAILED])
EventRule.objects.bulk_update(event_rules, ['event_types'])
class Migration(migrations.Migration):
dependencies = [
('extras', '0118_notifications'),
]
operations = [
migrations.AddField(
model_name='eventrule',
name='event_types',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=50),
blank=True,
null=True,
size=None
),
),
migrations.RunPython(
code=set_event_types,
reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name='eventrule',
name='event_types',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), size=None),
preserve_default=False,
),
migrations.RemoveField(
model_name='eventrule',
name='type_create',
),
migrations.RemoveField(
model_name='eventrule',
name='type_delete',
),
migrations.RemoveField(
model_name='eventrule',
name='type_job_end',
),
migrations.RemoveField(
model_name='eventrule',
name='type_job_start',
),
migrations.RemoveField(
model_name='eventrule',
name='type_update',
),
]

View File

@ -3,6 +3,7 @@ import urllib.parse
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.postgres.fields import ArrayField
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
@ -60,30 +61,9 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
max_length=200, max_length=200,
blank=True blank=True
) )
type_create = models.BooleanField( event_types = ArrayField(
verbose_name=_('on create'), base_field=models.CharField(max_length=50),
default=False, help_text=_("The types of event which will trigger this rule.")
help_text=_("Triggers when a matching object is created.")
)
type_update = models.BooleanField(
verbose_name=_('on update'),
default=False,
help_text=_("Triggers when a matching object is updated.")
)
type_delete = models.BooleanField(
verbose_name=_('on delete'),
default=False,
help_text=_("Triggers when a matching object is deleted.")
)
type_job_start = models.BooleanField(
verbose_name=_('on job start'),
default=False,
help_text=_("Triggers when a job for a matching object is started.")
)
type_job_end = models.BooleanField(
verbose_name=_('on job end'),
default=False,
help_text=_("Triggers when a job for a matching object terminates.")
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'), verbose_name=_('enabled'),
@ -145,9 +125,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
super().clean() super().clean()
# At least one action type must be selected # At least one action type must be selected
if not any([ if not self.event_types:
self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
]):
raise ValidationError( raise ValidationError(
_("At least one event type must be selected: create, update, delete, job start, and/or job end.") _("At least one event type must be selected: create, update, delete, job start, and/or job end.")
) )

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from extras.models import * from extras.models import *
from netbox.constants import EMPTY_TABLE_TEXT from netbox.constants import EMPTY_TABLE_TEXT
from netbox.events import get_event_text
from netbox.tables import BaseTable, NetBoxTable, columns from netbox.tables import BaseTable, NetBoxTable, columns
from .columns import NotificationActionsColumn from .columns import NotificationActionsColumn
@ -399,20 +400,10 @@ class EventRuleTable(NetBoxTable):
enabled = columns.BooleanColumn( enabled = columns.BooleanColumn(
verbose_name=_('Enabled'), verbose_name=_('Enabled'),
) )
type_create = columns.BooleanColumn( event_types = columns.ArrayColumn(
verbose_name=_('Create') verbose_name=_('Event Types'),
) func=get_event_text,
type_update = columns.BooleanColumn( orderable=False
verbose_name=_('Update')
)
type_delete = columns.BooleanColumn(
verbose_name=_('Delete')
)
type_job_start = columns.BooleanColumn(
verbose_name=_('Job Start')
)
type_job_end = columns.BooleanColumn(
verbose_name=_('Job End')
) )
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='extras:webhook_list' url_name='extras:webhook_list'
@ -422,12 +413,10 @@ class EventRuleTable(NetBoxTable):
model = EventRule model = EventRule
fields = ( fields = (
'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types', 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types',
'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'event_types', 'tags', 'created', 'last_updated',
'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update', 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'event_types',
'type_delete', 'type_job_start', 'type_job_end',
) )

View File

@ -1,6 +1,5 @@
import datetime import datetime
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
@ -13,6 +12,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Loca
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
from netbox.events import *
from users.models import Group, User from users.models import Group, User
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -113,9 +113,9 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
Webhook.objects.bulk_create(webhooks) Webhook.objects.bulk_create(webhooks)
event_rules = ( event_rules = (
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]), EventRule(name='EventRule 1', event_types=[OBJECT_CREATED], action_object=webhooks[0]),
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]), EventRule(name='EventRule 2', event_types=[OBJECT_CREATED], action_object=webhooks[1]),
EventRule(name='EventRule 3', type_create=True, action_object=webhooks[2]), EventRule(name='EventRule 3', event_types=[OBJECT_CREATED], action_object=webhooks[2]),
) )
EventRule.objects.bulk_create(event_rules) EventRule.objects.bulk_create(event_rules)
@ -123,7 +123,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'EventRule 4', 'name': 'EventRule 4',
'object_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
'action_object_id': webhooks[3].pk, 'action_object_id': webhooks[3].pk,
@ -131,7 +131,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'EventRule 5', 'name': 'EventRule 5',
'object_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
'action_object_id': webhooks[4].pk, 'action_object_id': webhooks[4].pk,
@ -139,7 +139,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{ {
'name': 'EventRule 6', 'name': 'EventRule 6',
'object_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
'action_object_id': webhooks[5].pk, 'action_object_id': webhooks[5].pk,

View File

@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from core.events import *
from dcim.choices import SiteStatusChoices from dcim.choices import SiteStatusChoices
from dcim.models import Site from dcim.models import Site
from extras.conditions import Condition, ConditionSet from extras.conditions import Condition, ConditionSet
@ -230,8 +231,7 @@ class ConditionSetTest(TestCase):
""" """
event_rule = EventRule( event_rule = EventRule(
name='Event Rule 1', name='Event Rule 1',
type_create=True, event_types=[OBJECT_CREATED, OBJECT_UPDATED],
type_update=True,
conditions={ conditions={
'attr': 'status.value', 'attr': 'status.value',
'value': 'active', 'value': 'active',
@ -251,8 +251,7 @@ class ConditionSetTest(TestCase):
""" """
event_rule = EventRule( event_rule = EventRule(
name='Event Rule 1', name='Event Rule 1',
type_create=True, event_types=[OBJECT_CREATED, OBJECT_UPDATED],
type_update=True,
conditions={ conditions={
"attr": "status.value", "attr": "status.value",
"value": ["planned", "staging"], "value": ["planned", "staging"],
@ -273,8 +272,7 @@ class ConditionSetTest(TestCase):
""" """
event_rule = EventRule( event_rule = EventRule(
name='Event Rule 1', name='Event Rule 1',
type_create=True, event_types=[OBJECT_CREATED, OBJECT_UPDATED],
type_update=True,
conditions={ conditions={
"attr": "status.value", "attr": "status.value",
"value": ["planned", "staging"], "value": ["planned", "staging"],
@ -300,8 +298,7 @@ class ConditionSetTest(TestCase):
webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST') webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST')
form = EventRuleForm({ form = EventRuleForm({
"name": "Event Rule 1", "name": "Event Rule 1",
"type_create": True, 'event_types': [OBJECT_CREATED, OBJECT_UPDATED],
"type_update": True,
"action_object_type": ct.pk, "action_object_type": ct.pk,
"action_type": "webhook", "action_type": "webhook",
"action_choice": webhook.pk, "action_choice": webhook.pk,

View File

@ -46,22 +46,22 @@ class EventRuleTest(APITestCase):
webhook_type = ObjectType.objects.get(app_label='extras', model='webhook') webhook_type = ObjectType.objects.get(app_label='extras', model='webhook')
event_rules = EventRule.objects.bulk_create(( event_rules = EventRule.objects.bulk_create((
EventRule( EventRule(
name='Webhook Event 1', name='Event Rule 1',
type_create=True, event_types=[OBJECT_CREATED],
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
), ),
EventRule( EventRule(
name='Webhook Event 2', name='Event Rule 2',
type_update=True, event_types=[OBJECT_UPDATED],
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
), ),
EventRule( EventRule(
name='Webhook Event 3', name='Event Rule 3',
type_delete=True, event_types=[OBJECT_DELETED],
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
@ -82,8 +82,7 @@ class EventRuleTest(APITestCase):
""" """
event_rule = EventRule( event_rule = EventRule(
name='Event Rule 1', name='Event Rule 1',
type_create=True, event_types=[OBJECT_CREATED, OBJECT_UPDATED],
type_update=True,
conditions={ conditions={
'and': [ 'and': [
{ {
@ -131,7 +130,7 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for the new object # Verify that a background task was queued for the new object
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.kwargs['event_rule'], EventRule.objects.get(type_create=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 1'))
self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED) self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data['id']) self.assertEqual(job.kwargs['data']['id'], response.data['id'])
@ -181,7 +180,7 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each new object # Verify that a background task was queued for each new object
self.assertEqual(self.queue.count, 3) self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs): for i, job in enumerate(self.queue.jobs):
self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_create=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 1'))
self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED) self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data[i]['id']) self.assertEqual(job.kwargs['data']['id'], response.data[i]['id'])
@ -212,7 +211,7 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for the updated object # Verify that a background task was queued for the updated object
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.kwargs['event_rule'], EventRule.objects.get(type_update=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 2'))
self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED) self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk) self.assertEqual(job.kwargs['data']['id'], site.pk)
@ -268,7 +267,7 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each updated object # Verify that a background task was queued for each updated object
self.assertEqual(self.queue.count, 3) self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs): for i, job in enumerate(self.queue.jobs):
self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_update=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 2'))
self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED) self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], data[i]['id']) self.assertEqual(job.kwargs['data']['id'], data[i]['id'])
@ -294,7 +293,7 @@ class EventRuleTest(APITestCase):
# Verify that a task was queued for the deleted object # Verify that a task was queued for the deleted object
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.kwargs['event_rule'], EventRule.objects.get(type_delete=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 3'))
self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED) self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk) self.assertEqual(job.kwargs['data']['id'], site.pk)
@ -327,7 +326,7 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each deleted object # Verify that a background task was queued for each deleted object
self.assertEqual(self.queue.count, 3) self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs): for i, job in enumerate(self.queue.jobs):
self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_delete=True)) self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 3'))
self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED) self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
self.assertEqual(job.kwargs['model_name'], 'site') self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], sites[i].pk) self.assertEqual(job.kwargs['data']['id'], sites[i].pk)
@ -342,7 +341,7 @@ class EventRuleTest(APITestCase):
A dummy implementation of Session.send() to be used for testing. A dummy implementation of Session.send() to be used for testing.
Always returns a 200 HTTP response. Always returns a 200 HTTP response.
""" """
event = EventRule.objects.get(type_create=True) event = EventRule.objects.get(name='Event Rule 1')
webhook = event.action_object webhook = event.action_object
signature = generate_signature(request.body, webhook.secret) signature = generate_signature(request.body, webhook.secret)

View File

@ -6,6 +6,7 @@ from django.test import TestCase
from circuits.models import Provider from circuits.models import Provider
from core.choices import ManagedFileRootPathChoices, ObjectChangeActionChoices from core.choices import ManagedFileRootPathChoices, ObjectChangeActionChoices
from core.events import *
from core.models import ObjectChange, ObjectType from core.models import ObjectChange, ObjectType
from dcim.filtersets import SiteFilterSet from dcim.filtersets import SiteFilterSet
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
@ -251,7 +252,7 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
class EventRuleTestCase(TestCase, BaseFilterSetTests): class EventRuleTestCase(TestCase, BaseFilterSetTests):
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
filterset = EventRuleFilterSet filterset = EventRuleFilterSet
ignore_fields = ('action_data', 'conditions') ignore_fields = ('action_data', 'conditions', 'event_types')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -292,11 +293,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 1', name='Event Rule 1',
action_object=webhooks[0], action_object=webhooks[0],
enabled=True, enabled=True,
type_create=True, event_types=[OBJECT_CREATED],
type_update=False,
type_delete=False,
type_job_start=False,
type_job_end=False,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
description='foobar1' description='foobar1'
), ),
@ -304,11 +301,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 2', name='Event Rule 2',
action_object=webhooks[1], action_object=webhooks[1],
enabled=True, enabled=True,
type_create=False, event_types=[OBJECT_UPDATED],
type_update=True,
type_delete=False,
type_job_start=False,
type_job_end=False,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
description='foobar2' description='foobar2'
), ),
@ -316,11 +309,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 3', name='Event Rule 3',
action_object=webhooks[2], action_object=webhooks[2],
enabled=False, enabled=False,
type_create=False, event_types=[OBJECT_DELETED],
type_update=False,
type_delete=True,
type_job_start=False,
type_job_end=False,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
description='foobar3' description='foobar3'
), ),
@ -328,22 +317,14 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 4', name='Event Rule 4',
action_object=scripts[0], action_object=scripts[0],
enabled=False, enabled=False,
type_create=False, event_types=[JOB_STARTED],
type_update=False,
type_delete=False,
type_job_start=True,
type_job_end=False,
action_type=EventRuleActionChoices.SCRIPT, action_type=EventRuleActionChoices.SCRIPT,
), ),
EventRule( EventRule(
name='Event Rule 5', name='Event Rule 5',
action_object=scripts[1], action_object=scripts[1],
enabled=False, enabled=False,
type_create=False, event_types=[JOB_COMPLETED],
type_update=False,
type_delete=False,
type_job_start=False,
type_job_end=True,
action_type=EventRuleActionChoices.SCRIPT, action_type=EventRuleActionChoices.SCRIPT,
), ),
) )
@ -384,25 +365,9 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
params = {'enabled': False} params = {'enabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_type_create(self): def test_event_type(self):
params = {'type_create': True} params = {'event_type': [OBJECT_CREATED, OBJECT_UPDATED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type_update(self):
params = {'type_update': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_delete(self):
params = {'type_delete': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_job_start(self):
params = {'type_job_start': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_job_end(self):
params = {'type_job_end': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests): class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):

View File

@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from core.events import *
from core.models import ObjectType from core.models import ObjectType
from dcim.models import DeviceType, Manufacturer, Site from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import * from extras.choices import *
@ -394,9 +395,9 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
site_type = ObjectType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
event_rules = ( event_rules = (
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]), EventRule(name='EventRule 1', event_types=[OBJECT_CREATED], action_object=webhooks[0]),
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]), EventRule(name='EventRule 2', event_types=[OBJECT_CREATED], action_object=webhooks[1]),
EventRule(name='EventRule 3', type_create=True, action_object=webhooks[2]), EventRule(name='EventRule 3', event_types=[OBJECT_CREATED], action_object=webhooks[2]),
) )
for event in event_rules: for event in event_rules:
event.save() event.save()
@ -406,9 +407,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = { cls.form_data = {
'name': 'Event X', 'name': 'Event X',
'object_types': [site_type.pk], 'object_types': [site_type.pk],
'type_create': False, 'event_types': ','.join([OBJECT_UPDATED, OBJECT_DELETED]),
'type_update': True,
'type_delete': True,
'conditions': None, 'conditions': None,
'action_type': 'webhook', 'action_type': 'webhook',
'action_object_type': webhook_ct.pk, 'action_object_type': webhook_ct.pk,
@ -418,8 +417,8 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"name,object_types,type_create,action_type,action_object", f'name,object_types,event_types,action_type,action_object',
"Webhook 4,dcim.site,True,webhook,Webhook 1", f'Webhook 4,dcim.site,"{OBJECT_CREATED}",webhook,Webhook 1',
) )
cls.csv_update_data = ( cls.csv_update_data = (
@ -430,7 +429,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'type_update': True, 'description': 'New description',
} }

View File

@ -19,6 +19,7 @@ from extras.choices import LogLevelChoices
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.registry import registry
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
@ -550,6 +551,14 @@ class EventRuleListView(generic.ObjectListView):
class EventRuleView(generic.ObjectView): class EventRuleView(generic.ObjectView):
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
def get_extra_context(self, request, instance):
return {
'event_types': [
event for name, event in registry['events'].items()
if name in instance.event_types
]
}
@register_model_view(EventRule, 'edit') @register_model_view(EventRule, 'edit')
class EventRuleEditView(generic.ObjectEditView): class EventRuleEditView(generic.ObjectEditView):

View File

@ -13,9 +13,28 @@ __all__ = (
'EVENT_TYPE_SUCCESS', 'EVENT_TYPE_SUCCESS',
'EVENT_TYPE_WARNING', 'EVENT_TYPE_WARNING',
'Event', 'Event',
'get_event',
'get_event_type_choices',
'get_event_text',
) )
def get_event(name):
return registry['events'].get(name)
def get_event_text(name):
if event := registry['events'].get(name):
return event.text
return ''
def get_event_type_choices():
return [
(event.name, event.text) for event in registry['events'].values()
]
@dataclass @dataclass
class Event: class Event:
name: str name: str
@ -26,6 +45,8 @@ class Event:
return self.text return self.text
def register(self): def register(self):
if self.name in registry['events']:
raise Exception(f"An event named {self.name} has already been registered!")
registry['events'][self.name] = self registry['events'][self.name] = self
def color(self): def color(self):

View File

@ -34,29 +34,25 @@
</table> </table>
</div> </div>
<div class="card"> <div class="card">
<h5 class="card-header">{% trans "Events" %}</h5> <h5 class="card-header">{% trans "Event Types" %}</h5>
<table class="table table-hover attr-table"> <ul class="list-group list-group-flush">
<tr> {% for name, event in registry.events.items %}
<th scope="row">{% trans "Create" %}</th> <li class="list-group-item">
<td>{% checkmark object.type_create %}</td> <div class="row align-items-center">
</tr> <div class="col-auto">
<tr> {% if name in object.event_types %}
<th scope="row">{% trans "Update" %}</th> {% checkmark True %}
<td>{% checkmark object.type_update %}</td> {% else %}
</tr> {{ ''|placeholder }}
<tr> {% endif %}
<th scope="row">{% trans "Delete" %}</th> </div>
<td>{% checkmark object.type_delete %}</td> <div class="col">
</tr> {{ event }}
<tr> </div>
<th scope="row">{% trans "Job start" %}</th> </div>
<td>{% checkmark object.type_job_start %}</td> </li>
</tr> {% endfor %}
<tr> </ul>
<th scope="row">{% trans "Job end" %}</th>
<td>{% checkmark object.type_job_end %}</td>
</tr>
</table>
</div> </div>
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>