Impact Tracking¶
The Impact model is what turns a Maintenance or Outage event from a free-form notice into structured data. Each Impact row is a (event, target object, impact level) tuple. Both ends use Django's GenericForeignKey, so a single Impact model handles every supported pair: Maintenance-on-Circuit, Maintenance-on-Device, Outage-on-Site, etc.
Model¶
| Field | Type | Notes |
|---|---|---|
event_content_type |
FK -> ContentType |
Limited to notices.maintenance and notices.outage. |
event_object_id |
PositiveInteger (indexed) | PK of the linked event. |
event |
GenericForeignKey | Resolved Maintenance or Outage instance. |
target_content_type |
FK -> ContentType |
Must be in allowed_content_types. |
target_object_id |
PositiveInteger (indexed) | PK of the affected NetBox object. |
target |
GenericForeignKey | Resolved target instance (Circuit, Device, Site, ...). |
impact |
CharField (choices, nullable) | Severity level. See Impact Levels. |
sites |
M2M -> dcim.Site |
Cached site membership of the target. Populated by refresh_sites(). |
locations |
M2M -> dcim.Location |
Cached location membership of the target. |
tags |
M2M -> NetBox Tag | Standard tagging. |
A unique_together constraint on (event_content_type, event_object_id, target_content_type, target_object_id) prevents linking the same event to the same target more than once.
Impact levels¶
Defined in notices/choices.py:ImpactTypeChoices (BCOP standard).
| Level | Colour | Meaning |
|---|---|---|
NO-IMPACT |
green | Maintenance affects this object's path but no observable degradation. |
REDUCED-REDUNDANCY |
yellow | Object remains in service but with one or more redundant paths offline. |
DEGRADED |
orange | Object is in service but not at full capacity / quality. |
OUTAGE |
red | Object will be entirely down. |
Impact level may be null (no level recorded yet); the Impact table sorts by level so unknowns appear first by default.
Allowed target types¶
Validation in Impact.clean() rejects any target_content_type that is not present in PLUGINS_CONFIG["notices"]["allowed_content_types"]. The check is case-insensitive but stores the canonical lowercase form. Forms only offer types from this list.
If you change allowed_content_types, restart NetBox so the dynamically-registered template extensions and form choices update.
Site and location cache¶
Filtering "show me every maintenance affecting any device in site X" naively requires walking every Impact's target GenericForeignKey, then dispatching to model-specific code to resolve a Site. That doesn't scale and you can't index it.
The plugin solves this with two cached M2M fields on Impact:
Impact.sites-- the set of Sites the target belongs to.Impact.locations-- the set of Locations the target belongs to.
Both are populated by Impact.refresh_sites(), which calls into the resolver registry in notices/resolvers.py -- a per-content-type lookup of small functions that know how to extract Site/Location PKs from an instance of that type.
refresh_sites() is called automatically by notices.signals whenever:
- An Impact is saved.
- A target object is saved (so a Device moving to a new Site updates every Impact pointing at it).
- A target object is deleted (cascade clears the M2M).
The list filters on Maintenance and Outage (site_id, region_id, site_group_id, location_id) traverse these cached M2Ms, which makes them fast, indexable, and capable of region/site-group rollups for free.
Adding new content types¶
If you add a content type to allowed_content_types that does not have a resolver registered, notices.checks emits a startup warning, and impacts on objects of that type will have empty sites/locations -- they will be invisible to the site-scoped filters.
See Site and Location Resolvers for how to register a resolver for a custom type.
Validation rules¶
Impact.clean() enforces:
target_content_typemust be inallowed_content_types.event_content_typemust benotices.maintenanceornotices.outage.- The parent event must not be in a frozen state. You cannot add or modify Impacts on Maintenance with status
COMPLETED/CANCELLEDor Outage with statusRESOLVED.
Rule 3 is the audit-integrity guarantee: once an event is closed, its impact ledger is immutable.
Audit trail¶
Impact.to_objectchange() overrides NetBox's default to set related_object to the parent event. This causes Impact changes to appear in the event's changelog, so the maintenance or outage detail page shows a unified timeline of "added impact for Circuit XYZ at 14:32, removed impact for Device ABC at 14:35".
The same trick is used for EventNotification.to_objectchange().
REST API¶
GET /api/plugins/notices/impact/
POST /api/plugins/notices/impact/
GET /api/plugins/notices/impact/{id}/
PATCH /api/plugins/notices/impact/{id}/
PUT /api/plugins/notices/impact/{id}/
DELETE /api/plugins/notices/impact/{id}/
Creating an Impact¶
The serializer requires the content-type integers, not the model name strings, on input. Resolve them once at parser startup:
import requests
response = requests.get(
"https://netbox.example.com/api/extras/content-types/?app_label=notices&model=maintenance",
headers={"Authorization": f"Token {token}"},
)
maintenance_ct = response.json()["results"][0]["id"]
Then create the Impact:
POST /api/plugins/notices/impact/
Authorization: Token YOUR_API_TOKEN
Content-Type: application/json
{
"event_content_type": 245,
"event_object_id": 17,
"target_content_type": 12,
"target_object_id": 412,
"impact": "OUTAGE"
}
The response includes nested representations for both event and target:
{
"id": 88,
"url": "https://.../api/plugins/notices/impact/88/",
"event": { "id": 17, "name": "MAINT-2026-001", "status": "CONFIRMED", ... },
"target": { "id": 412, "cid": "ZAYO/CKT/12345", ... },
"impact": "OUTAGE",
"tags": [],
"created": "2026-04-27T13:01:55Z",
"last_updated": "2026-04-27T13:01:55Z"
}
When the target is a Circuit, the full CircuitSerializer is used; for other types, a minimal {id, name, type} shape is returned (per ImpactSerializer.get_target).
Filtering Impacts¶
The ImpactFilterSet filters on id, event_content_type, event_object_id, target_content_type, target_object_id, and impact level. To find every impact attached to maintenance #17, filter on event_object_id=17 and event_content_type=<maintenance ct>.
The richer site-scoped filters live on the parent event filtersets (MaintenanceFilterSet, OutageFilterSet) and traverse the Impact.sites / Impact.locations cache; see Maintenance and Outage.
UI behaviour¶
There is no top-level "Impacts" list view; impacts are managed in the context of their parent event:
- Maintenance / Outage detail page shows the impact table with Sites and Locations columns derived from the cached M2M.
- + Add Impact opens
ImpactEditViewwith the parent event pre-filled. - Object detail pages (Device, Circuit, Site, ...) show the inverse: a "Maintenance and Outage History" widget listing every event that has ever impacted this object within the
event_history_dayswindow.
The per-object widget is registered dynamically: at app-ready time, notices.template_content reads allowed_content_types and creates one PluginTemplateExtension subclass per type.