Architecture¶
This document describes the internal architecture of the NetBox FMS plugin. It is intended for developers who want to understand, extend, or contribute to the codebase.
Data Model Hierarchy¶
NetBox FMS follows the same Type/Instance pattern used by NetBox's own
DeviceType / Device relationship. A FiberCableType acts as a blueprint that
defines cable construction and component templates. When a FiberCable instance is
created, it reads those templates and auto-instantiates the corresponding physical
components.
erDiagram
FiberCableType ||--o{ BufferTubeTemplate : "has"
FiberCableType ||--o{ RibbonTemplate : "has (central-core)"
FiberCableType ||--o{ CableElementTemplate : "has"
BufferTubeTemplate ||--o{ RibbonTemplate : "has (ribbon-in-tube)"
FiberCableType ||--o{ FiberCable : "instantiates"
FiberCable ||--|| Cable : "extends (dcim)"
FiberCable ||--o{ BufferTube : "contains"
FiberCable ||--o{ Ribbon : "contains (central-core)"
FiberCable ||--o{ FiberStrand : "contains (tight buffer)"
FiberCable ||--o{ CableElement : "contains"
BufferTube ||--o{ Ribbon : "contains"
BufferTube ||--o{ FiberStrand : "contains"
Ribbon ||--o{ FiberStrand : "contains"
SpliceProject ||--o{ SplicePlan : "groups"
SplicePlan }o--|| Device : "for closure (dcim)"
SplicePlan ||--o| User : "submitted_by"
SplicePlan ||--o{ SplicePlanEntry : "contains"
SplicePlanEntry ||--|| FrontPort : "fiber_a (dcim)"
SplicePlanEntry ||--|| FrontPort : "fiber_b (dcim)"
ClosureCableEntry ||--|| Device : "on (dcim)"
ClosureCableEntry ||--|| FiberCable : "cable"
FiberCircuit ||--o{ FiberCircuitPath : "has"
FiberCircuitPath }o--|| FiberStrandPath : "assigns"
FiberStrandPath ||--o{ FiberStrandPathHop : "has"
FiberStrandPathHop }o--o| FiberStrand : "strand"
FiberStrandPathHop }o--o| Cable : "cable (dcim)"
FiberStrandPathHop }o--o| Circuit : "provider circuit (circuits)"
FiberCircuit ||--o{ RouteChangeAuthorization : "authorized by"
PathAnomaly }o--o| FiberStrand : "strand"
PathAnalysisQueue }o--|| Device : "device (dcim)"
Model Groups¶
Models are organized into four functional groups:
1. Type-level (blueprints)¶
These models define the construction of a fiber cable without representing a physical instance.
| Model | Purpose |
|---|---|
FiberCableType |
Top-level blueprint. Defines fiber count, jacket color, cable type, and owns component templates. Counter-cached instance count via connect_counters(). |
BufferTubeTemplate |
Template for a buffer tube within a cable type. Specifies fiber count or holds child RibbonTemplate entries. |
RibbonTemplate |
Template for a fiber ribbon. Can be a child of a BufferTubeTemplate (ribbon-in-tube) or directly on a FiberCableType (central-core ribbon). |
CableElementTemplate |
Template for non-fiber elements such as strength members or messenger wires. |
2. Instance-level¶
Created automatically by FiberCable._instantiate_components() when a new
FiberCable is saved. Four construction cases are supported:
| Case | Structure |
|---|---|
| Loose tube | BufferTube with FiberStrand children |
| Ribbon-in-tube | BufferTube -> Ribbon -> FiberStrand |
| Central-core ribbon | Ribbon (on cable) -> FiberStrand |
| Tight buffer | FiberStrand directly on cable |
| Model | Purpose |
|---|---|
FiberCable |
Instance linked one-to-one with a dcim.Cable. Triggers component instantiation on creation. |
BufferTube |
Physical buffer tube within a cable. |
Ribbon |
Fiber ribbon, either inside a tube or directly in a cable. |
FiberStrand |
Individual fiber strand. Automatically assigned colors based on FiberCableType's color_scheme. |
CableElement |
Non-fiber element instance (strength member, tracer wire, etc.). |
3. Splice planning¶
| Model | Purpose |
|---|---|
SpliceProject |
Groups related splice plans for project-level organization. |
SplicePlan |
A splice plan for a specific closure (dcim.Device, ForeignKey -- multiple plans per closure allowed). Tracks diff staleness for cache invalidation. Status follows a four-state FSM: draft -> pending_approval -> approved -> archived. Stores submitted_by (FK to auth.User). Enforces fiber exclusivity: each FrontPort may appear in at most one approved plan at a time. |
SplicePlanEntry |
Maps one dcim.FrontPort (fiber_a) to another dcim.FrontPort (fiber_b), representing a planned splice. Includes a change_note field for per-entry annotations. |
ClosureCableEntry |
Records which FiberCable instances enter a closure (dcim.Device). |
SlackLoop |
Tracks slack loop storage at a closure. |
4. Fiber circuits¶
| Model | Purpose |
|---|---|
FiberCircuit |
End-to-end logical circuit spanning multiple fiber segments and splices. |
FiberCircuitPath |
A circuit's assignment of one analyzed FiberStrandPath: the assigned-hops snapshot, active, is_broken / broken_reason, loss fields. |
FiberStrandPath |
One continuous fiber chain between two classified ends, derived by the analysis (internal model, read-only UI and API). Carries completeness, route_key, is_proposed and is_defective. |
FiberStrandPathHop |
One ordered step of a path: a strand, a plain cable (no FiberCable) or a provider circuit. |
PathAnomaly |
A plant shape the analysis refused to trace (too many connections, loop, dangling reference). |
RouteChangeAuthorization |
Records that an approved change may re-route a circuit without breaking it; consumed by the analysis. |
PathAnalysisQueue |
One row per plant change per touched device, waiting for the next analysis run. |
Splice Plan Lifecycle¶
State Machine¶
A SplicePlan moves through four states:
| Transition | Meaning |
|---|---|
draft -> pending_approval |
Submitter requests review; submitted_by is recorded. |
pending_approval -> approved |
Reviewer approves the plan; diff may now be applied. |
pending_approval -> draft |
Reviewer rejects or submitter retracts; plan is editable again. |
approved -> archived |
Plan has been applied and is no longer active. |
Enforcement Points¶
State transition guards are checked in three places:
SplicePlan.clean()-- Validates that the requested status transition is legal and that the fiber exclusivity invariant is satisfied before the model is saved.- API views -- Dedicated action endpoints (
/submit/,/approve/,/archive/) enforce that callers supply the correct current state before permitting a transition. - Visual editor -- The splice editor widget disables mutation controls
(add/remove/reorder entries) when the plan is not in the
draftstate.
Fiber Exclusivity Invariant¶
A given FrontPort may appear as fiber_a or fiber_b in at most one
approved SplicePlan at a time. This constraint is enforced in
SplicePlan.clean() before the plan transitions to approved. Plans in other
states (draft, pending_approval, archived) do not participate in the
exclusivity check, so the same port can be planned in multiple drafts
simultaneously.
Service Layer¶
Business logic is separated from Django model methods into dedicated service modules.
services.py -- Splice plan computation and cable topology¶
compute_diff(plan)-- Compares the desired splice state (fromSplicePlanEntryrecords) against the live state (actualPortMappingentries in NetBox) and returns a per-tray breakdown of additions, removals, and unchanged splices. Pairs touching device-level front ports (unassigned tubes) are grouped under theUNASSIGNED_TRAY_ID = 0sentinel bucket instead of a tray.get_live_state(closure)/get_desired_state(plan)-- Query helpers that feedcompute_diff(). Both consider every front port of the closure.apply_diff(plan)-- Applies the computed diff to NetBox'sPortMappingtable, creating and deleting mappings as needed. Refuses plans that are not inapprovedstatus and archives the plan after a successful apply, so every apply path honours the approval workflow.import_live_state(plan)-- Imports the current live splice state into a plan's entries. Runs automatically when a plan is created through the web form or the API, so a new plan starts as a copy of the live state instead of an implicit delete-everything. The import is an idempotent sync: pairs the plan's own entries already cover are skipped, as are live pairs with no tray-mounted port (an entry requires a tray) and pairs on fibers claimed by another active plan; the returned report carries a count for each skip reason.link_cable_topology(cable, fiber_cable_type, device, ...)-- Atomic transaction that creates aFiberCable, adopts or createsFrontPort/RearPortpairs on the device, and sets the cable profile.propose_port_mapping()-- Builds a position-based mapping from strand positions toFrontPortinstances for confirmation before linking.
path_graph.py -- Graph loader and walker¶
Bulk-loads the affected plant region (cable terminations, port mappings,
strand landings, splice jumpers) into a networkx graph and walks it in
memory from every front port to produce chains of hops with classified ends.
The analysis jobs and the trace view share this one implementation.
path_analysis.py -- Result writer, reconcile and lock¶
Matches walked chains to stored FiberStrandPath rows, rewrites, creates or
deletes them in bulk, records anomalies, evaluates assignments (broken,
authorized, path_lost) and syncs FiberCircuit.is_broken. Also holds the
PostgreSQL advisory lock that serializes analysis runs and the whole-plant
reconcile.
path_queue.py -- Change queue¶
enqueue_devices(device_ids, reason) buffers per transaction and writes
PathAnalysisQueue rows on commit; schedule_analysis() enqueues the
Fiber path analysis job one window later unless one is already pending.
jobs.py -- Background jobs¶
PathAnalysisJob (incremental, per queued device) and PathReconcileJob
(system job, whole plant, registered in ready() with the configured
interval).
assignment.py -- Picker and assignment¶
find_assignable_path_groups() is the shared picker (groups of paths on one
route, filtered and ranked); assign_paths() creates assignments;
acknowledge_route() accepts new hops; authorize_route_change() writes a
RouteChangeAuthorization. The *_for(user, ...) variants add the
permission checks used by the UI and API.
trace.py and trace_hops.py -- Pairing rule and display hops¶
trace.py holds only the rule that pairs rear ports across a cable (by
connector number) for the walker. trace_hops.py
builds the display hops of the trace view from a stored path
(flat_entries, build_hops).
export.py -- Draw.io diagram generation¶
generate_drawio(plan)-- Creates mxGraph XML for a splice plan. Each splice tray becomes a separate page/tab in the diagram. Fibers are colored using each strand's stored color (assigned from the cable type's color scheme), and diff annotations (add/remove/unchanged) are applied to splice connections.
constants.py -- Fiber color schemes¶
EIA_598_COLORS-- Tuple of 12(hex_color, name)pairs following the EIA/TIA-598 standard fiber color code (Blue, Orange, Green, Brown, Slate, White, Red, Black, Yellow, Violet, Rose, Aqua).NBR_14771_COLORS-- Tuple of 12(hex_color, name)pairs following the ABNT NBR-14771 Brazilian standard (same colors, different position order).DIN_IEC_60304_COLORS-- Tuple of 12(hex_color, name)pairs following the German DIN VDE 0888 fiber sequence built on the IEC 60304 color set (Red, Green, Blue, Yellow, White, Grey, Brown, Violet, Turquoise, Black, Orange, Pink).COLOR_SCHEME_PALETTES-- Dict mapping scheme choices to color tuples, enabling multi-standard support.get_strand_color(position, scheme)-- Returns the color tuple for a 1-indexed fiber position under the given color scheme, cycling through the 12-color palette for positions beyond 12.
cable_profiles.py -- High-count fiber cable profiles¶
NetBox's built-in cable profiles cap at 16 positions (Single1C16P). Fiber plant
cables commonly have far more strands. This module defines BaseCableProfile
subclasses for 24, 48, 72, 96, 144, 216, 288, and 432 strand counts, allowing the
trace algorithm to route at the individual strand level through high-count fiber
trunk cables.
Signal Handlers¶
signals.py¶
Registered via connect_signals() in PluginConfig.ready().
cable_post_save-- Ondcim.Cablesave, checks whether the cable terminates onFrontPortinstances belonging to a closure with aSplicePlan. If so, marks the plan's diff as stale (diff_stale=True) so it will be recomputed on next access. Also renames ports via_rename_ports_for_cable()when a cable is linked to aFiberCable.cable_pre_delete-- Same invalidation logic, but onpre_delete(before cascade removes the termination records needed for the device lookup).portmapping_pre_save/portmapping_pre_delete-- Block external changes to FMS-managedPortMappingrows, preventing out-of-band edits that would bypass splice plan enforcement. A mapping is FMS-managed when aFiberStrandhas landed on its front port, when any mapping on its rear port serves such a strand, or when a non-archivedSplicePlansplices its front port. Other port pairs on the same device stay user-editable, so a housing can mix FMS-managed cables with unmanaged ones. Internal operations that legitimately need to writePortMappingrecords must use thefms_portmapping_bypasscontext manager to suppress the guard.fibercable_post_save-- Syncs port names on the associated closure device when aFiberCableis linked to adcim.Cable.closure_cable_entry_post_delete-- Cleans up orphanedTubeAssignmentrecords when aClosureCableEntryis removed.
Template Content Injection¶
template_content.py¶
Uses NetBox's PluginTemplateExtension system to inject content into core NetBox
detail views.
CableFiberCablePanel-- Adds a panel to thedcim.Cabledetail page showing the associatedFiberCableinstance, its type, and tube count. Only renders when aFiberCableis linked to the cable.
Plugin Registration¶
__init__.py¶
The NetBoxFMSConfig class (subclass of PluginConfig) serves as the plugin
entry point.
| Setting | Value |
|---|---|
base_url |
"fms" |
min_version |
"4.5.0" |
The ready() method performs two setup actions:
connect_signals()-- Registers thepost_saveandpre_deletehandlers fordcim.Cabledescribed above.connect_counters(FiberCableType)-- Enables NetBox's built-in counter cache so thatFiberCableTypetracks how manyFiberCableinstances reference it without requiring a count query on every list view.