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:

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:


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:

Optional fields:

Compute options (optional array, controls expensive analyses):

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:

Traffic shift fields:

BGP transition fields (in bgp_shifts.summary.transitions[]):

Error responses:


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:


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:

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:


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:

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:


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:


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:


Delete Scenario
DELETE /api/v1/simulation-scenarios/{id}

Auth: Admin, Engineer (must be owner).

Response: 204 No Content

Error responses: