Simulation (Engineering Mode)
Get Simulation Bundle
GET /api/v1/simulation/bundle?pi_id=<uuid>
Returns all topology and route data needed for client-side SPF simulation as a single JSON response. Contains Cytoscape elements (devices + links), inter-area routes, external routes, and ASBR entries for the specified protocol instance. The frontend uses this bundle to perform Dijkstra SPF without additional API calls.
Auth: Any authenticated user.
Query parameters:
pi_id (required) -- Protocol instance UUID
Response: 200 OK
{
"elements": [
{ "group": "nodes", "data": { "id": "...", "router_id": "10.0.0.1", ... } },
{ "group": "edges", "data": { "id": "...", "source": "...", "target": "...", "cost_a_to_b": 10, ... } }
],
"inter_area_routes": [ ... ],
"external_routes": [ ... ],
"asbr_entries": [ ... ]
}
Error responses:
400 Bad Request -- Missing pi_id parameter
Evaluate Simulation
POST /api/v1/simulation/evaluate
Applies a set of topology mutations (link failures, node failures, cost changes, hypothetical links) to the live topology, runs multi-area SPF on both baseline and mutated graphs, and returns structural analysis: isolated devices, path changes, single points of failure (Tarjan articulation points), affected links, and optional traffic redistribution estimates. Server-side computation ensures correct multi-area OSPF routing per RFC 2328. Cached Dijkstra results (pathContext with sync.Map) and pre-built per-area adjacency maps enable sub-second evaluation. Cross-PI link resolution via resolveLinkDevicePairs handles mixed OSPFv2/v3 edges from the frontend.
Auth: Any authenticated user.
Request body:
{
"pi_id": "uuid-protocol-instance",
"mutations": [
{ "type": "node_failure", "device_id": "uuid-device" },
{ "type": "link_failure", "link_id": "uuid-link" },
{ "type": "cost_change", "link_id": "uuid-link", "direction": "both", "new_cost_forward": 100, "new_cost_reverse": 100 },
{ "type": "hypothetical_link", "source_device_id": "uuid-a", "target_device_id": "uuid-b", "cost_forward": 50, "cost_reverse": 50, "area_id": "uuid-area" },
{ "type": "hypothetical_node", "device_id": "hypnode-abc12345", "display_name": "NewRouter" },
{ "type": "srlg", "srlg_id": "uuid-srlg-group" },
{ "type": "peer_failure", "peer_id": "uuid-bgp-peer", "as_id": "uuid-as" }
],
"compute": ["traffic_estimate"],
"at": "2026-03-10T14:30:00Z"
}
Mutation types:
node_failure — Remove device and all its links. Requires device_id.
link_failure — Remove a specific link. Requires link_id.
cost_change — Change link cost. Requires link_id. Optional direction (both, forward, reverse), new_cost_forward, new_cost_reverse.
hypothetical_link — Add a synthetic link between two existing devices. Requires source_device_id, target_device_id, cost_forward, cost_reverse, area_id.
hypothetical_node — Register a synthetic device in the topology. Requires device_id. Optional display_name. The node has no area membership until connected with hypothetical_link mutations. Processed before other mutations (two-pass ordering).
srlg — Expand a saved SRLG (Shared Risk Link Group) into individual link_failure mutations for all member links. Requires srlg_id. Server-side expansion. Unresolvable or empty SRLGs are silently dropped.
peer_failure — Simulate BGP peer failure. Requires peer_id (bgp_peer UUID) and as_id (autonomous system UUID). Finds all prefixes where the failed peer is the current best-path source, removes that peer's candidates, re-runs RFC 4271 best-path selection, and returns per-prefix shift details. iBGP peer failures set is_approximation: true (real iBGP reconvergence depends on route reflector topology). Response includes bgp_shifts object with transitions (count of prefixes that shifted), unreachable_count, is_approximation, and shifts array with per-prefix detail including decision_step explaining why the new best was selected. failed_peers[] entries carry source ("bmp"/"snmp"); SNMP-discovered sessions (BGP4-MIB) are excluded from the RIB math — they have no RIB visibility, so instead of a misleading "0 affected prefixes" the summary carries a notes[] entry ("RIB impact cannot be computed") and is_approximation: true.
Optional fields:
at (string, RFC 3339) — Evaluate against historical topology at the given timestamp instead of live topology. Enables combined time-travel + engineering mode. BGP effects at T require full-RIB history: when a BMP target recorded history_mode="full" covering the scope at T, peer_failure and hot-potato are evaluated as-of-T against bgp_rib_entry_history; otherwise they are suppressed exactly as before — peer_failure mutations go to skipped_mutations and a notices advisory explains that no full-RIB history exists at T. BGP-traffic projection is always omitted in time-travel mode (it layers on live traffic). EIGRP failure filtering uses only primary/installed-ECMP evidence from the per-device snapshots covering T; a gap makes the direction opaque and never falls back to live state.
Compute options (optional array, controls expensive analyses):
traffic_estimate — Include traffic redistribution estimation using the Tomogravity model (Zhang/Roughan 2003). Per-pair demand estimated as V(i)·V(j)/V_total (device volumes from SNMP interface utilization), calibrated against observed per-link loads. Differential path analysis: only links that change between baseline and mutated paths receive deltas. Results area-scoped to areas containing failures (with 0.5% utilization delta noise floor).
Response: 200 OK
{
"isolated_devices": [
{ "device_id": "uuid", "router_id": "10.0.0.5", "hostname": "edge-03", "dns_name": "edge-03.ams.example.com", "label": "edge-03" }
],
"path_changes": [
{
"source_device_id": "uuid-src", "dest_device_id": "uuid-dst",
"source_label": "core-01", "dest_label": "edge-05",
"before_cost": 20, "after_cost": 30,
"before_route_type": "O", "after_route_type": "O IA",
"before_hops": ["10.0.0.1", "10.0.0.2", "10.0.0.3"],
"after_hops": ["10.0.0.1", "10.0.0.4", "10.0.0.3"],
"before_link_ids": ["uuid-l1", "uuid-l2"],
"after_link_ids": ["uuid-l3", "uuid-l4"]
}
],
"spofs": [
{ "device_id": "uuid", "router_id": "10.0.0.2", "hostname": "abr-01", "dns_name": "abr-01.ams.example.com", "label": "abr-01", "isolated_count": 3 }
],
"affected_links": ["uuid-link-1", "uuid-link-2"],
"traffic_shifts": [
{
"link_id": "uuid-link-3",
"source_device_id": "uuid-a", "dest_device_id": "uuid-b",
"source_label": "core-01", "dest_label": "core-02",
"before_utilization_pct": 45.2, "after_utilization_pct": 79.0,
"delta_pct": 33.8,
"before_bps": 4520000000, "after_bps": 7900000000,
"capacity_mbps": 10000,
"congestion_risk": "warning",
"is_failed": false
},
{
"link_id": "uuid-link-1",
"source_device_id": "uuid-c", "dest_device_id": "uuid-d",
"source_label": "core-03", "dest_label": "core-04",
"before_utilization_pct": 12.5, "after_utilization_pct": 0,
"delta_pct": -12.5,
"before_bps": 1250000000, "after_bps": 0,
"capacity_mbps": 10000,
"congestion_risk": "ok",
"is_failed": true
}
],
"compute_time_ms": 12.5
}
Response fields:
isolated_devices[].label, spofs[].label — Device display name resolved per the display.device_name_mode system setting. dns_name and hostname are also included as raw values.
source_label, dest_label — In path_changes and traffic_shifts, device display names resolved per the system setting.
- No convergence estimate is returned: Osprey cannot establish the actual failure-detection and convergence timing from the available observations, including whether BFD is running.
skipped_mutations (array, optional) — Mutations that were not applied, each { index, type, reason }. Emitted for peer_failure mutations in time-travel mode when no full-RIB history covers the scope at T (see at), or when a peer had no recorded session at T.
notices (array of strings, optional) — Non-fatal advisories about the evaluation, e.g. that BGP effects were not evaluated because no full-RIB history exists at the requested time.
Traffic shift fields:
before_bps, after_bps (float64) — Absolute traffic in bits per second before/after mutation.
is_failed (bool) — True for links that failed due to mutations. Failed links always have after_bps: 0.
congestion_risk — ok (< 80%), warning (80-95%), critical (>= 95%). Based on post-mutation utilization.
- Results are area-scoped: only links in areas containing failures are included. Shifts below 0.5% utilization delta are filtered.
BGP transition fields (in bgp_shifts.summary.transitions[]):
new_peer_id (string) — UUID of the surviving peer receiving redistributed prefixes.
new_peer_ip (string) — IP address of the surviving peer.
new_peer_as (int) — AS number of the surviving peer.
prefix_count (int) — Number of prefixes redistributed to this peer.
mapping_method (string) — Traffic data source for this peer: "exact" (SNMP interface match by IP), "subnet" (SNMP match by subnet containment), or "prefix_count_fallback" (estimated from prefix count ratios). Present when traffic projection is computed.
Error responses:
400 Bad Request -- Missing pi_id, empty mutations, invalid mutation type, missing required mutation fields
404 Not Found -- Protocol instance has no areas
Failure Assessment
POST /api/v1/simulation/failure-assessment
Batch failure assessment: for every link or every node in the protocol instance, compute the impact of its individual failure. Returns a sorted table suitable for CSV export.
Osprey's own recorders are excluded, in both directions — the same recorder-exclusion rule GET /diagnostics/spof applies, so the two redundancy surfaces cannot answer the same question differently. A GRE recorder attaches by a single max-metric tunnel, so bridge detection correctly finds that tunnel is a bridge; reporting it would tell an architect that the monitoring is a critical link. Recorders are therefore dropped from the assessed entity set and from the stranded-device counts — without the second half, every router hosting a recorder is reported as a single point of failure. A genuinely single-homed router is still reported.
Auth: Admin, Engineer.
Request body:
{
"pi_id": "uuid-protocol-instance",
"target": "all_links",
"include_traffic": false,
"at": "2026-03-10T14:30:00Z"
}
| Field |
Type |
Required |
Description |
pi_id |
UUID |
Yes |
Protocol instance scope |
target |
string |
Yes |
"all_links", "all_nodes", or "all_bgp_peers" |
include_traffic |
bool |
No |
Include traffic estimation (slower). Default false |
mutations |
array |
No |
Effective-baseline mutations to apply before the batch assessment (same shape as /simulation/evaluate) |
at |
string (RFC 3339) |
No |
Assess the historical snapshot at this timestamp instead of live topology (combined time-travel + engineering mode). target="all_bgp_peers" is live-only — with at set it is not evaluated as-of-T; the response returns an empty results and a notice instead (use GET /bgp/peers/{id}/impact-summary?at= for per-peer as-of-T analysis) |
Response: 200 OK
{
"target": "all_links",
"total_entities": 3,
"results": [
{
"entity_type": "link",
"entity_id": "uuid",
"entity_label": "rtr-core-01 <-> rtr-core-02",
"unreachable_devices": 2,
"unreachable_device_labels": ["rtr-edge-01", "rtr-edge-02"]
}
],
"computation_time_ms": 312
}
notice (optional) is set when a target could not be evaluated as requested — currently target="all_bgp_peers" with at set (batch BGP peer-failure is live-only in time-travel); results is then empty.
Only failures that cause actual device isolation are included in results. Redundant links/nodes whose failure causes no isolation are filtered out. Results sorted by unreachable_devices descending.
Error responses:
400 Bad Request -- Missing pi_id, invalid target value
404 Not Found -- Protocol instance has no areas
Compute Simulation Path
POST /api/v1/simulation/path
Computes a device-to-device path on a mutated topology. Applies mutations to the live topology, then runs the same area-aware path computation as GET /api/v1/topology/path on the mutated graph. Returns both forward and reverse paths. Cross-domain pairs retain the normal BGP-stitched path_kind: "multi-domain" result even when the selected protocol excludes the far endpoint: the protocol filter scopes a same-domain SPF question, not the other ASes in a multi-domain chain.
Mutations use the same vocabulary and per-type required fields as /simulation/evaluate (node_failure, link_failure, cost_change, hypothetical_link, hypothetical_node, srlg, peer_failure) and are validated the same way — an unknown type is a 400. peer_failure is accepted but is a no-op here: it is not an IGP mutation and does not alter the path graph.
Auth: Any authenticated user.
Request body:
{
"pi_id": "uuid-protocol-instance",
"source_id": "uuid-source-device",
"dest_id": "uuid-dest-device",
"mutations": [
{ "type": "node_failure", "device_id": "uuid-device" }
],
"at": "2026-03-10T14:30:00Z",
"area_ids": ["uuid-ospf-area", "uuid-eigrp-area"],
"protocol": "ospfv2",
"address_family": "ipv4"
}
Optional fields:
at (string, RFC 3339) — Compute path on historical topology at the given timestamp instead of live topology.
area_ids (UUID array) — Explicit canvas area scope; preferred over pi_id for a multi-protocol selection.
protocol (string) — Protocol toggle for the source/same-domain SPF view. A positively identified cross-domain path automatically restores the other domains from area_ids.
source_addr, dest_addr (string) — Selected endpoint addresses; used to keep cross-domain stitching in the requested address family.
address_family (ipv4, ipv6, or clns) — Explicit path address family.
Response: 200 OK — Same PathResult structure as GET /api/v1/topology/path (forward_path + reverse_path with hops, total_cost, route_type, area_segments).
SPF projection, not the forwarding chain: simulated paths — including each link-state segment of a stitched multi-domain result — are the source-rooted least-cost tree on the mutated graph, and the response says so in warning. A post-mutation hop-by-hop chain is not computable — the Type-3 summaries the surviving ABRs would re-originate are exactly what cannot be synthesised — and rendering only the baseline as a chain would make a before/after diff report the model change instead of the mutation. Both sides therefore stay on the projection, consistently. The same note appears in the evaluate endpoint's notices whenever it returns path_changes.
EIGRP scopes and segments: arbitrary what-if EIGRP re-convergence cannot be computed — the observed chain is the routers' own DUAL result and re-convergence depends on composite metrics that are not readable over SNMP. The endpoint is precise about what it does know. For pure failures (node_failure/link_failure) it filters the observed primary plus the router's RIB-installed ECMP chains: an untouched primary stays in place, while a surviving already-installed equal-cost brother may be promoted without predicting DUAL. This also applies to an EIGRP leg inside a stitched multi-domain path. If no installed chain survives — or for any cost_change/hypothetical_link, which can attract a distance-vector path from anywhere — the endpoint returns no EIGRP path (or an opaque EIGRP domain in a partial multi-domain stitch) plus a warning explaining the remaining DUAL limitation, never a fabricated SPF answer.
When at is present, primary, installed ECMP variants, source-border choice and
both border-ranking rails share one request-scoped historical EIGRP resolver.
Each device is read/decoded and deep-copied into the cache at most once; cache
hits borrow that immutable value read-only. The resolver is only a
concurrency-safe cache; independent accumulators keep forward, reverse, each
segment and each border comparison separate, while direction parents absorb
their child evidence without double-counting devices. A coverage miss makes
only the affected direction/domain/comparison opaque; an operational history
error returns 500 for the whole response.
No selection rail may promote another candidate merely because the preferred
candidate's historical EIGRP evidence is unavailable.
When that refusal occurs during the initial source-border selection, the plain
cross-domain direction remains the response shape and carries
eigrp_history.reason=history_unavailable, unavailable_device_id, and an
explanation step saying that border selection was refused.
Successful rendered ECMP variants contribute their device samples and time
bounds, deduplicated with the primary. Simulation aggregates evidence after
filtering and promotion: a promoted branch carries its own temporal status and
coverage horizon, and discarded branches contribute neither temporal nor
route-level RIB evidence. Unknown optional tails still weaken only ECMP while
an otherwise covered primary survives. Historical caveats say sampled historical EIGRP chain; promotion states that the branch was recorded as installed by the selected historical sample, never that it is observed live.
Error responses:
400 Bad Request -- Missing required fields, source == destination
404 Not Found -- Protocol instance has no areas, source/destination not found in mutated topology
Simulation Scenarios
List Scenarios
GET /api/v1/simulation-scenarios?scope_type=protocol_instance&scope_id=<uuid>
Returns scenarios owned by the current user plus shared scenarios for the specified scope, ordered by updated_at DESC.
Auth: Any authenticated user.
Query parameters:
scope_type (required) -- protocol_instance or network
scope_id (required) -- UUID of the scoping entity
limit / offset -- Pagination (default: 50 / 0)
Response: 200 OK -- Paginated array
{
"data": [
{
"id": "uuid",
"user_id": "uuid",
"username": "admin",
"scope_type": "protocol_instance",
"scope_id": "uuid",
"name": "Core link failure",
"description": "Simulates backbone link failure between core-01 and core-02",
"mutations": [ ... ],
"mutation_count": 3,
"shared": true,
"created_at": "2026-03-10T10:00:00Z",
"updated_at": "2026-03-12T14:30:00Z"
}
],
"total": 5,
"limit": 50,
"offset": 0
}
Create Scenario
POST /api/v1/simulation-scenarios
Auth: Admin, Engineer.
Request body:
{
"scope_type": "protocol_instance",
"scope_id": "uuid",
"name": "Core link failure",
"description": "Optional description",
"mutations": [ { "type": "link_failure", "link_id": "uuid" } ]
}
| Field |
Type |
Required |
Description |
scope_type |
string |
Yes |
"protocol_instance" or "network" |
scope_id |
UUID |
Yes |
Scoping entity ID |
name |
string |
Yes |
1-100 characters, unique per user per scope |
description |
string |
No |
Up to 1000 characters |
mutations |
JSON array |
Yes |
Opaque mutation array (stored as-is) |
Response: 201 Created -- Full scenario object with generated id, created_at, updated_at.
Error responses:
400 Bad Request -- Validation errors (missing name, invalid scope_type, etc.)
403 Forbidden -- Role not admin or engineer
409 Conflict -- Duplicate name for user+scope
Get Scenario
GET /api/v1/simulation-scenarios/{id}
Auth: Any authenticated user (must be owner or scenario must be shared).
Response: 200 OK -- Full scenario object.
Error responses:
404 Not Found -- Scenario not found
Update Scenario
PUT /api/v1/simulation-scenarios/{id}
Auth: Admin, Engineer (must be owner).
Request body:
{
"name": "Updated name",
"description": "Updated description",
"mutations": [ ... ],
"shared": true
}
Response: 200 OK -- Updated scenario object.
Error responses:
400 Bad Request -- Validation errors
403 Forbidden -- Not the scenario owner
404 Not Found -- Scenario not found
409 Conflict -- Duplicate name
Delete Scenario
DELETE /api/v1/simulation-scenarios/{id}
Auth: Admin, Engineer (must be owner).
Response: 204 No Content
Error responses:
403 Forbidden -- Not the scenario owner
404 Not Found -- Scenario not found