Fiber Circuits¶
A FiberCircuit is the end-to-end logical service that runs over your
fiber plant. Where a dcim.Cable represents one physical span, a fiber
circuit ties together all of the spans, splices, and ports that carry one
service from origin to destination, with optional parallel paths for
diversity. Circuits are first-class NetBox objects with their own status
lifecycle, REST API, GraphQL type, change log, and search index.
The circuit subsystem has three responsibilities:
- Discovery. Given two devices, find candidate paths through the fiber graph (cables + closures + existing splices), score them, and return ranked proposals.
- Provisioning. Materialize a chosen proposal as a
FiberCircuitplus oneFiberCircuitPathper strand, atomically. - Protection. Once a circuit is active, prevent its underlying resources (cables, ports, strands, splice entries) from being deleted or re-spliced out from under it.
Data model¶
erDiagram
FiberCircuit ||--o{ FiberCircuitPath : has
FiberCircuitPath ||--o{ FiberCircuitNode : has
FiberCircuitPath }o--|| FrontPort : "origin (dcim)"
FiberCircuitPath }o--o| FrontPort : "destination (dcim)"
FiberCircuitNode }o--o| Cable : "cable (dcim)"
FiberCircuitNode }o--o| FrontPort : "front_port (dcim)"
FiberCircuitNode }o--o| RearPort : "rear_port (dcim)"
FiberCircuitNode }o--o| FiberStrand : "fiber_strand"
FiberCircuitNode }o--o| SplicePlanEntry : "splice_entry"
FiberCircuit¶
The top-level service object.
| Field | Type | Notes |
|---|---|---|
name |
char(200) | Display name, required |
cid |
char(200) | External circuit identifier (work order, internal CID, etc.) |
status |
choice | planned, staged, active, decommissioned |
description |
text | Free-form description |
strand_count |
positive int | Number of parallel strand paths the circuit reserves |
tenant |
FK -> tenancy.Tenant |
Optional tenant attribution |
comments |
text | Long-form notes |
A circuit's strand_count caps how many FiberCircuitPath rows can hang
off it. Saving the circuit with status=decommissioned deletes all
FiberCircuitNode rows so the underlying objects are no longer protected.
Moving a decommissioned circuit back to any other status rebuilds nodes by
calling FiberCircuitPath.rebuild_nodes() from the stored path JSON.
FiberCircuitPath¶
One strand's journey from origin to destination.
| Field | Type | Notes |
|---|---|---|
circuit |
FK -> FiberCircuit |
Owning circuit |
position |
positive int | 1-indexed ordering within the circuit |
origin |
FK -> dcim.FrontPort |
Path entry point |
destination |
FK -> dcim.FrontPort |
Path exit point (nullable for incomplete traces) |
path |
JSON list | Ordered hop records (front_port / rear_port / cable / splice_entry) |
is_complete |
bool | True if the trace reached a destination |
calculated_loss_db |
decimal(6,3) | Optional design-time loss budget |
actual_loss_db |
decimal(6,3) | Optional measured loss (OTDR, power meter) |
wavelength_nm |
positive int | Wavelength the loss values apply to; required if either loss is set |
(circuit, position) is unique. clean() enforces the strand-count cap and
that wavelength_nm is set whenever calculated_loss_db or actual_loss_db
is populated.
FiberCircuitNode¶
A relational index of every object on a path. Each node references one of
cable, front_port, rear_port, fiber_strand, or splice_entry via
on_delete=PROTECT. This is what enforces circuit protection: as long as
the circuit is not decommissioned, deleting any of those underlying objects
raises ProtectedError.
Nodes are not edited directly. They are rebuilt automatically on path retrace, on circuit status transitions, and during provisioning.
Status lifecycle¶
| Status | Meaning |
|---|---|
planned |
Circuit has been designed but not yet staged. Nodes exist; resources are protected. |
staged |
Circuit is ready for cutover; pre-deployment activities are in progress. |
active |
Circuit is live and carrying traffic. |
decommissioned |
Circuit is no longer in service. Nodes are deleted; resources are no longer protected. |
There is no enforced state-machine: any transition is permitted, and the
side effects on FiberCircuitNode rows (delete on entering
decommissioned, rebuild on leaving) happen automatically in save().
Discovery: find_fiber_paths()¶
The discovery engine lives in netbox_fms.provisioning. The public entry
point is:
from netbox_fms.provisioning import find_fiber_paths
proposals = find_fiber_paths(
origin_device,
destination_device,
strand_count=1,
priorities=None, # default: ["hop_count", "new_splices",
# "strand_adjacency", "lowest_strand"]
max_results=20,
)
Algorithm¶
- Build a device graph. All cables that terminate on
RearPortinstances are collected. Each cable becomes a bidirectional edge between the two devices its rear ports belong to. - Enumerate simple routes. Depth-first search finds every simple path (no repeated devices) between origin and destination, up to a default max depth of 10.
- Compute hop availability. For each hop along each route, look at the
PortMappings on both ends and find the
RearPortpositions that have matching FrontPorts on entry and exit, with neither FrontPort already occupied (terminated by a non-zero-length cable). - Generate candidates. For single-hop routes, every group of
strand_countavailable positions on the same cable is a candidate. For multi-hop routes, the engine builds chains: each chain enters a closure on one cable and exits on the next, with a splice (existing or to-be-created) at the intermediate device. - Score and rank. Each candidate is scored by the priority list in order. Lower is better.
Scoring priorities¶
| Priority | Meaning |
|---|---|
hop_count |
Fewer cable spans win |
new_splices |
Reuse existing splices over creating new ones |
strand_adjacency |
Prefer strands that are contiguous within a cable |
lowest_strand |
Prefer lower-numbered strand positions (deterministic ordering) |
Pass a different ordering or subset to change the ranking. For example, if you care more about reusing splices than minimizing hops:
proposals = find_fiber_paths(
origin, dest, strand_count=2,
priorities=["new_splices", "hop_count", "strand_adjacency"],
)
Proposal shape¶
Each proposal is a dict:
{
"strands": [
{
"hops": [
{
"cable_id": 42,
"fp_entry_id": 101,
"fp_exit_id": 105,
"entry_rp_id": 71,
"exit_rp_id": 72,
"position": 7,
},
# ...
],
"position": 7,
},
# one entry per requested strand
],
"route": [12, 34, 56], # ordered device IDs
"hop_count": 2,
"new_splice_count": 1,
"existing_splice_count": 0,
"is_contiguous": True,
"lowest_position": 7,
"splices_needed": [
{"device_id": 34, "fp_a_id": 105, "fp_b_id": 110},
],
}
Provisioning: create_circuit_from_proposal()¶
Once a proposal is selected, materialize it atomically:
from netbox_fms.provisioning import create_circuit_from_proposal
circuit = create_circuit_from_proposal(
proposals[0],
name="DC1-DC2 backbone A", # or use name_template
name_template="Circuit-{n}", # auto-incrementing fallback
splice_project=my_project, # optional SpliceProject for new splices
)
Inside a single transaction, the helper:
- Creates the
FiberCircuitwithstatus=plannedandstrand_countmatching the number of strands in the proposal. - Creates one
FiberCircuitPathper strand, populatingorigin,destination,path(the hop JSON), andis_complete=True. - Creates
FiberCircuitNoderows for every front port, rear port, cable, and splice entry on the path, plus one node perFiberStrandderived from the path's front ports. - For each
splices_neededentry, creates a zero-lengthdcim.CablewithFrontPort<->FrontPortterminations (a splice cable) and a matchingSplicePlanEntry. Ifsplice_projectis provided, a draft plan is created or reused for that closure under that project; otherwise the first existing plan on the closure is used.
If the transaction fails for any reason, no circuit, paths, nodes, splice cables, or plan entries are created.
The classmethod helpers FiberCircuit.find_paths() and
FiberCircuit.create_from_proposal() proxy to the provisioning module if
you prefer the model-side API.
Tracing and retracing¶
The trace engine lives in netbox_fms.trace. From a starting FrontPort
it walks the chain FrontPort -> PortMapping -> RearPort ->
CableTermination -> Cable -> remote RearPort -> PortMapping -> FrontPort,
crossing splices (SplicePlanEntry) when one is present. Loops are
detected and stop the trace.
FiberCircuitPath.from_origin(front_port) returns an unsaved path with
the trace populated. FiberCircuitPath.retrace() re-runs the trace from
self.origin, updates path / destination / is_complete, saves, and
either rebuilds protection nodes (active circuit) or deletes them
(decommissioned circuit).
The REST API exposes both:
# Per-path hop-by-hop trace with computed totals
GET /api/plugins/fms/fiber-circuit-paths/{id}/trace/
# Retrace every path on a circuit
POST /api/plugins/fms/fiber-circuits/{id}/retrace/
The trace endpoint returns {circuit_id, circuit_name, path_position,
is_complete, hops, total_calculated_loss_db, total_actual_loss_db,
wavelength_nm} along with hop records suitable for rendering in the UI's
trace view.
Loss budgets¶
Each path tracks two loss values:
calculated_loss_db: design-time estimate based on fiber attenuation per kilometer, splice losses, and connector losses.actual_loss_db: measured value from OTDR or power meter testing.
Both fields are decimal(6,3). When either is set, wavelength_nm is
required so the loss is interpretable. Compare planned versus measured
loss to spot bad splices or damaged fiber, or to validate that the circuit
is within receiver sensitivity for the deployed transceivers.
The plugin does not currently compute losses automatically: enter the
calculated value manually based on your loss budget tooling, then update
actual_loss_db after field testing.
Circuit protection¶
Circuit protection is what stops you from breaking a live service while
modifying NetBox. Every object on a non-decommissioned circuit's path has a
matching FiberCircuitNode with on_delete=PROTECT, so:
- Deleting a
dcim.Cablereferenced by an active path raisesProtectedError. - Deleting a
FrontPortorRearPortcarrying an active circuit raisesProtectedError. - Bulk-updating splice plan entries that touch a protected fiber returns
HTTP 409 with a body listing the conflicting circuit names. See
/api/plugins/fms/splice-plans/{id}/bulk-update/and the closure Pending Work tab for the user-facing surface.
The dedicated endpoint GET /api/plugins/fms/fiber-circuits/protecting/
takes one or more of cable=, front_port=, rear_port=,
fiber_strand=, or splice_entry= (comma-separated IDs) and returns the
circuits whose paths reference any of them. Use it to surface "which
circuits would be affected" in dashboards or pre-flight checks.
To take resources out from under protection, set the circuit to
decommissioned. The save() override deletes its FiberCircuitNode
rows in the same transaction.
Common workflows¶
Provision a new dark-fiber circuit between two POPs¶
from dcim.models import Device
from netbox_fms.provisioning import find_fiber_paths, create_circuit_from_proposal
origin = Device.objects.get(name="POP-A-CLOSURE")
dest = Device.objects.get(name="POP-B-CLOSURE")
proposals = find_fiber_paths(origin, dest, strand_count=2,
priorities=["hop_count", "new_splices"])
best = proposals[0]
circuit = create_circuit_from_proposal(best, name="POP-A <-> POP-B (Pair 1)")
Cut a circuit over to a new path¶
- Create the new circuit with
create_circuit_from_proposal(). It entersplannedstatus, so its nodes are protected immediately. - Verify the new path with
POST .../{id}/retrace/. - Decommission the old circuit (set status to
decommissioned). Its protection nodes are deleted, freeing the previously held splices. - Set the new circuit to
active.
Inventory all circuits affected by a planned outage¶
curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX_URL/api/plugins/fms/fiber-circuits/protecting/?cable=42,43,44"
The response is a standard fiber-circuit list, suitable for a customer notification spreadsheet or a maintenance ticket.