Scripts triggered by Event Rules now respect notifications_default and
job_timeout from script Meta class. Updates documentation to clarify
this behavior and adds regression test coverage.
Fixes#22852
* Closes#22770: Allow plugins to register Event Rule action handlers
Introduces an EventRuleAction registration API (netbox.event_rules /
netbox.extras.event_rules) so plugins can add new EventRule action types
the same way they already register search indexes and event types,
replacing the hardcoded webhook/script/notification elif-chain. Core's
own three action types are refactored onto this mechanism.
An EventRule referencing an unregistered action (e.g. its providing
plugin is uninstalled) remains stored, is skipped during processing
without affecting other rules, is visibly marked unavailable in the
UI/API, and triggers a new extras.W001 system check, resuming
automatically once the plugin is reinstalled, with no need to re-save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Fix CI failure: extras.W001 check must tolerate a not-yet-migrated database
check_event_rule_actions() queried EventRule unconditionally, which broke
`manage.py makemigrations --check` (and a fresh `manage.py migrate`) on a
database with no tables yet, since Django runs system checks before
verifying/applying migrations. Wrap the query and swallow DatabaseError,
matching the existing check_postgresql_version precedent for a database
that may not be ready. Verified against a fresh, unmigrated database that
makemigrations --check, migrate, and manage.py check all behave correctly,
and that the warning still fires once a qualifying EventRule exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Fix EventRuleForm action_type widget: HTMXSelect was silently ignored
Meta.widgets only applies to fields the ModelForm auto-generates; action_type
is an explicit class-level field, so its HTMXSelect assignment in Meta.widgets
never took effect, and switching Action type in the browser never refreshed
the action_choice field's label/queryset. Move the widget onto the field
declaration itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address review feedback from Jeremy Stretch on PR #22793
- Revert action_object_type on_delete to CASCADE (was changed to SET_NULL)
- Make action_type choices dynamic via the model field's own callable
choices=, simplifying EventRule.clean() and making any unavailable
action_type invalid on save, whether new or unchanged
- Rename is_action_available to action_is_available
- Fold new dispatch tests into the existing RQQueueTestMixin test class to
fix a flaky --parallel run (two such classes cross-flush each other's
Redis queues)
- Only catch broad exceptions around plugin-provided actions in
process_event_rules(); let a core action's own bugs propagate
- Add value_action_type() so table exports don't leak the "unavailable"
badge's HTML markup
- Drop the frozen CSVChoiceField on action_type and make action_object
optional at the field level, so bulk import of an object-less action
works
- Map ValidationErrors on unexposed model fields to NON_FIELD_ERRORS in
bulk import instead of letting them surface as a raw ValueError
- Restore EventRuleActionEnum/the enum-based GraphQL filter, built from
the live action registry instead of the static EventRuleActionChoices
- Default EventRuleAction.object_required to False, matching
object_model's default of None; set it explicitly on the three core
actions
- Drop the unused request parameter on get_object_queryset()
- Fix action_object_type's serializer queryset, which incorrectly used
the triggering object_types' feature flag
- Use .format() instead of % in get_action_type_display()
- Remove the extras.W001 system check (a DB query on every management
command) in favor of an action_is_available field on the REST API
- Raise ValidationError instead of a bare Exception on duplicate action
slug registration
- Shorten a couple of overly verbose inline comments
- Split EventRuleAction.validate() into an internal _validate() and a
public no-op validate(), so a subclass's custom validation doesn't
need to remember to call super()
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Trim verbose comments/docstrings added while addressing review feedback
Shortened a number of overly long inline comments and test docstrings
introduced across the previous commit's review-feedback fixes (the
EventRuleActionEnum comment, the _validate()/validate() docstrings, and
several test docstrings that restated context already given elsewhere).
Also drops the auto-generated "Generated by Django" header comment from
migration 0143, matching the rest of this app's hand-touched migrations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from automated follow-up review of #22793
- Clear stale action_object_type/action_object_id when an action declares
object_model but is left with no object selected (object_required=False);
previously neither branch of the if/elif fired and the old value from
before the edit silently persisted. Fixed in both EventRuleForm and
EventRuleImportForm (the latter matters for CSV updates of an existing
row). Also resolve the content type from the actual selected object
rather than the action's declared object_model, correctly handling
subclass/proxy instances.
- Validate action slugs at registration time (format, and collision via
enum_key() with an already-registered slug) so a bad third-party slug
is rejected immediately instead of crashing GraphQL schema assembly at
startup.
- Strip the dead-code label/description duplication out of
EventRuleActionChoices.CHOICES -- nothing reads it, and it risked
drifting from WebhookAction/ScriptAction/NotificationAction's own.
- Record whether an action is plugin-provided at registration time
instead of introspecting its module on every dispatch; core's three
actions now register with is_plugin_provided=False explicitly.
- Add an action_is_available filter (API + UI) so event rules with a
now-unavailable action can still be found in bulk, now that the
extras.W001 system check is gone.
- Update the plugin dev docs: fix the OpenTicketAction example (it was
missing object_required=True, the exact gap the action_object fix
above addresses), note that an unavailable rule can't be saved at all
(not just skipped), and move an internal-only note out of the
published class docstring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from second automated follow-up review of #22793
- Drop the ChoiceSet base from EventRuleActionChoices. With CHOICES=(),
the previous version made ChoiceField(choices=EventRuleActionChoices)
-- the idiomatic pattern used for every other ChoiceSet in this
codebase, and reachable via `from extras.choices import *` -- silently
reject every value instead of failing at first use.
- Reject slugs containing hyphens or a leading underscore at
registration time: a hyphenated slug (plausible, since plugin
distribution names are conventionally hyphenated) or a slug starting
with an underscore both currently pass validation but produce a
GraphQL-invalid or GraphQL-reserved enum member name once sanitized,
crashing schema assembly at startup. Document the constraint in the
plugin docs and the published slug docstring.
- Raise ImproperlyConfigured instead of ValidationError for all three
registration-failure cases in register_event_rule_action() -- these
are packaging/configuration mistakes surfaced from AppConfig.ready(),
not user input, matching the convention ChoiceSetMeta already uses for
the same class of error.
- Remove is_plugin_provided's class-level default; nothing reads it
before an action is registered in any real code path, and the default
masked a class-vs-instance inconsistency. Move its documentation out
of the published Attributes docstring into a plain comment.
- Simplify EventRuleImportForm.clean()'s action_object_type/id
assignment to match EventRuleForm.clean()'s approach (set both fields
once, unconditionally, from the resolved object) rather than assigning
via the GFK setter and then conditionally overwriting the content
type.
- Split a dense doc sentence in eventrule.md onto its own line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address findings from third automated follow-up review of #22793
- Restore is_plugin_provided's class-level default of True. Its only
read is inside process_event_rules()'s exception handler; without a
default, an action reaching dispatch without going through
register_event_rule_action() (e.g. inserted into the registry dict
directly) raised AttributeError while already handling the real
exception, masking it entirely instead of degrading gracefully.
- Move the slug/label presence checks out of __init_subclass__ (which
fired at class-definition time, raising TypeError) and into
register_event_rule_action() as ImproperlyConfigured, unifying them
with the other three registration-time checks. This also resolves a
still-open item from the very first automated review: an intermediate
base class shared by several concrete plugin actions couldn't
previously be defined without a placeholder slug/label of its own.
- Restore the GFK assignment (self.instance.action_object = obj) in
EventRuleImportForm.clean() alongside the explicit content-type
assignment, so EventRule.clean()'s later access to action_object hits
the descriptor cache instead of an extra SELECT per imported row.
- Clarify the slug docstring/docs wording (leading underscore
specifically, not underscores in general; tell authors to use an
underscore instead of a hyphen) and document that intermediate base
classes are now supported. Add a test for an uppercase slug.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Document the frozen-at-import tradeoff on EventRuleSerializer.action_type
The EventRuleActionAPITestCase docstring in test_api.py pointed here for
an explanation of why the choices are materialized once at module-import
time rather than dynamically, but the field itself had no such comment.
* Address findings from fourth automated follow-up review of #22793
- Restore the "must start with a letter" slug constraint dropped from
the docs page and class docstring by the previous round's rewording;
reword to "must begin with a lowercase letter", which covers the
leading-digit case SLUG_RE actually rejects and matches the
ImproperlyConfigured message an author will hit.
- Scope the GFK-cache-priming comment in EventRuleImportForm.clean() to
the non-proxy case it actually holds for, rather than claiming it
unconditionally.
- Trim comments and docstrings that had regrown into reviewer-facing
rationale (why a prior finding was reverted, why a check isn't in
__init_subclass__ anymore rather than API documentation, in
event_rules.py and test_event_rules.py.
EOF
)
* Misc cleanup
* Misc cleanup
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Introduces production PyPI publishing triggered by v* tag pushes, while
Test PyPI now requires manual dispatch. Both indexes never receive the
same run, ensuring proper separation between rehearsal and production.
Fixes#22786
Clarifies that backslashes in constraint values must be escaped in JSON.
Includes example showing regex pattern escaping and adds table entry
demonstrating regex constraint usage.
Fixes#22498
Introduce experimental Python package installation workflow as an
alternative to release archive and Git methods. Document package layout,
setup command, upgrade procedure, and migration path for existing
deployments.
Fixes#22604
Add initial Python package support for NetBox, including wheel and sdist
builds, generated package metadata, and Test PyPI publishing for maintainer
validation.
Add package-aware CLI support, `netbox setup` scaffolding for instance-local
files, and centralized wheel-vs-checkout path handling while preserving the
existing source/archive install layout.
Bundle pre-rendered embedded documentation in the wheel, and extend CI to
verify dependency pins, wheel metadata, artifact contents, CLI behavior, sdist
rebuilds, and smoke-test upgrades.
Expose an event's prechange and postchange snapshots to event rule
condition evaluation, making snapshots.prechange.<attr> and
snapshots.postchange.<attr> available through the existing dot-path
syntax.
Add changed and unchanged snapshot operators for comparing an attribute
across the two snapshots without requiring a condition value. These
operators support rules such as firing only when a field transitions to a
specific state.
Make condition values optional only for snapshot operators by introducing
a missing-value sentinel, while preserving value requirements for all
other operators. Reject invalid combinations such as using changed or
unchanged with an explicit value or with an attr starting with snapshots.
Fail closed when condition paths traverse invalid snapshot structures,
including raw scalar snapshot values such as status strings, by treating
unresolvable snapshot-operator paths as missing and converting invalid
direct paths to InvalidCondition.
Document the new snapshot path syntax, changed and unchanged operators,
create/delete snapshot behavior, and the serialization differences
between snapshot data and REST API data. Add regression and integration
tests covering validation, transition behavior, null snapshot edge cases,
direct snapshot paths, and event rule evaluation.
Add PUT/PATCH support to ScriptModuleViewSet for replacing Script Module
content in place. Modules can be addressed by numeric ID or file name,
and the uploaded file name must match the existing file path.
The module's scripts are re-synchronized from the new content after
successful update.
Fixes#22544
* refactor(forms): Add GenericObjectChoiceField
Replace separate scope_type/scope and parent_object_type/parent field
pairs with unified GenericObjectChoiceField. Introduce
GenericObjectFormMixin to handle GFK descriptor initialization and
assignment.
This removes redundant HTMX/queryset setup logic from ScopedForm,
VLANGroupForm, and ServiceForm by delegating GFK presentation to a
single reusable field and mixin pair. Field query param references now
use `$scope_object_id` instead of `$scope` to match the subwidget name.
Fixes#19821
* fix(forms): Skip validation on HTMX bulk-edit dependent field refresh
Render bulk-edit form unbound when an HTMX request changes a dependent
field (e.g. content type) without clicking Apply. This prevents
validation errors from surfacing before the user submits.
Cache ContentType lookups in GenericObjectChoiceField and sync widget
references before setting queryset to ensure choices land on the
rendered subwidget.
* fix(ipam): Update scope query params for GenericObjectChoiceField
Change available-prefix Add links to use `scope_content_type` and
`scope_object_id` query parameters instead of `scope_type` and `scope`.
This aligns with the GenericObjectChoiceField subwidget naming
introduced in the earlier refactor.
* refactor(models): Simplify GFK handling in clone_fields
Replace `scope_type`/`scope_id` pairs with bare `scope` GFK names in
clone_fields across models. Update CloningMixin to emit GFK subwidget
parameters (`scope_content_type`, `scope_object_id`) directly when a
GenericForeignKey appears in clone_fields.
* Update pre-populated links
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Defer CachedValue updates from post_save/post_delete signal handlers to a
SearchCacheJob that runs after the surrounding transaction commits. Coalesce
pending updates per database alias and savepoint scope, preserving rollback
semantics while reducing synchronous write latency.
When no worker is available, or Redis cannot be reached during dispatch, apply
the update inline so existing single-process installs continue to behave
correctly. Replay the originating database alias during deferred reads and
writes so cache updates remain routed to the schema that emitted the signal.
Keep deferral private to CachedValueSearchBackend so custom SEARCH_BACKEND
implementations continue to use the existing synchronous cache/remove contract.
Move the job runner to search/jobs.py and keep the CachedValue-specific update
logic on the backend.
Document the eventual consistency of global search results and add coverage for
coalescing, rollback/savepoint behavior, worker dispatch, inline fallback,
deleted objects, and the custom backend contract.
Adds a nullable DurationField computed as completed - started, set in
Job.terminate(). Exposes it as an orderable table column, detail-panel
attr, REST API field, and UI/API range filters (execution_time__gte /
execution_time__lte).
A plain stored field (not a GeneratedField) keeps the migration
metadata-only, avoiding a full table rewrite on core_job.
* #21025: WIP
* Fixes#22357: Remove unused `local_context_data` field from dcim.Module (#22364)
* Add partial index for checking null CC data
* Ensure the data returned by get_config_context() is safe for mutation
* Implement selective backup queryset annotation to avoid n+1 overhead on cold cache
* Fix migration conflict
* Replace MPTT with Ltree per #21418
Bulk write operations (create/update/delete a JSON list at a model's list
endpoint) can opt into background processing with the ?background=true query
parameter. The request is validated synchronously and, if accepted, an
AsyncAPIJob is enqueued and a 202 Accepted is returned with the job id and
poll URL; the write is performed later by a worker that re-invokes the same
viewset action, so behavior matches the synchronous path (including
all-or-nothing transaction semantics).
- AsyncAPIJob reconstructs the request in the worker, re-applies object
permissions, runs within the request processors (change logging/events),
and captures the action's response into job.data as {status_code, data}.
- Handled rejections are translated to match the synchronous API: APIException
via handle_exception(), and AbortRequest/ProtectedError/RestrictedError via a
new NetBoxModelViewSet.exception_to_response() helper. These terminate the
job as "failed" (reserving "errored" for unexpected crashes).
- Background processing is refused with 503 when no worker is servicing the
queue, and rejected with 400 when combined with an If-Match precondition
(which cannot be honored once execution is deferred).
- Single-object writes, GET requests, and non-list payloads ignore the
parameter and run synchronously.
exception_to_response() intentionally duplicates the translation logic in
dispatch() rather than dispatch() being refactored to call it; consolidating
the two is left as a follow-up to keep this change off the synchronous hot path.
* Address code review feedback (#21992)
- Carry the request's scheme and host into the background worker so absolute
URLs in the captured job result point at the real server instead of a
hardcoded http://localhost/.
- Emit the same protected-delete warning log in exception_to_response() that
dispatch() produces, restoring application-log parity for background failures.
- Drop the inert `_authenticator = None` assignment: setting request.user
already prevents lazy re-authentication via the public API, and nothing on
the worker's action path reads the authenticator.
- Remove the redundant success-path job.save() (JobRunner.handle() ->
terminate() persists job.data) and hoist the AsyncAPIJob import in mixins.py
to module level (no real import cycle through it).
- Add a test asserting result URLs reflect the request host.
* Fix IPv6 host parsing in background API request reconstruction
Parse the carried host with urlsplit (and pass it verbatim as HTTP_HOST)
instead of host.partition(':'), which split bracketed IPv6 hosts like
[::1]:8443 on their inner colons. Extract request construction into
AsyncAPIJob._build_request and add a test asserting the IPv6 host round-trips.
* Address review feedback (#21992)
- Make the bulk mixins safe to use without BackgroundOperationMixin: guard the
_background_requested / _maybe_background_bulk_create calls with a getattr
fallback so BulkUpdateModelMixin/BulkDestroyModelMixin/SequentialBulkCreatesMixin
retain their standalone behavior in custom viewset composition.
- Add a test covering the background ProtectedError/RestrictedError path: a bulk
delete of a protected object records the same 409 the synchronous API returns
(job failed, status_code 409, object preserved), via exception_to_response().
Follow-up to #22363: align the plugin hook names with the already-renamed
JINJA_FILTERS setting (#22288) and with the rest of the codebase's 'Jinja'
spelling convention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add support for literal `{lat}` and `{lon}` placeholders in `MAPS_URL`
when rendering GPS coordinate links. Existing configurations continue to
work by falling back to appending `lat,lon` when no coordinate placeholders
are present.
Move map URL handling into shared UI helpers so `GPSCoordinatesAttr` and
`AddressAttr` use consistent placeholder detection. When `MAPS_URL` contains
coordinate placeholders, suppress address-based map links to avoid rendering
invalid URLs.
Add tests for placeholder replacement, decimal coordinate values, fallback
behavior, and address link suppression. Also document the address link behavior
in the `MAPS_URL` configuration description.
Adds warning and examples for validating NetBox objects before saving
in Custom Scripts. Direct ORM writes bypass UI/API validation and can
introduce invalid data. Recommends calling `full_clean()` explicitly.
Fixes#22249
Fix miscounting of total_vlan_ids when VLANGroup vid_ranges use
non-canonical bounds (e.g. '[]'). Normalize ranges to '[)' on save and
add migration to recompute existing totals. Prevent division-by-zero in
utilization queries for legacy rows with miscounted totals.
Fixes#22228
Allow IP ranges where start_address equals end_address to model
single-IP pools like DHCP or NAT reservations. Add validation tests,
filterset coverage, and display logic to render both endpoints.
Fixes#21993
Display a warning in the UI whenever a user goes to provision a v1
token (both via the admin token form and the user profile token form).
Update documentation to note that v1 tokens are deprecated and will be
removed in NetBox v5.0.
Removes the deprecated querystring template tag from utilities/templatetags/
helpers.py and updates all 30 call sites across templates to use Django's
built-in querystring tag (available since Django 5.1). The request argument
is dropped since the built-in tag reads from the template context automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostgreSQL 14 reaches end-of-life in November 2026 and Django 6.1 will
require PostgreSQL 15+. Updates all documentation references from 14 to
15, and removes the "needed on PostgreSQL 15 and later" conditional
comment from the database creation instructions (since 15 is now the
minimum).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the deprecated registry['models'] key, the __getitem__ deprecation
warning, and the population code in register_model(). Registered models
should be retrieved via ObjectType.objects.public() instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the LEGACY_ACTIONS constant and ActionsMixin._convert_legacy_actions()
method. Action views must now define the actions attribute as a list of
ObjectAction subclasses rather than as a legacy permission dict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes SENTRY_DSN, SENTRY_SAMPLE_RATE, SENTRY_SEND_DEFAULT_PII, and
SENTRY_TRACES_SAMPLE_RATE. These were superseded by SENTRY_CONFIG in
v4.4.2. Documentation updated to use SENTRY_CONFIG exclusively.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>