MPLS TE Tunnels¶
On top of YAML-based topologies and
Traffic Engineering attributes, a diagram can declare
MPLS TE tunnels (RSVP-TE or SR-TE style) in a top-level lsps: section.
Topolograph runs CSPF (Constrained Shortest Path First) placement over them —
same bandwidth/affinity/SRLG constraints as a real router, without any
signaling — and visualizes the result.

The LSP tunnels tab lists every path with its placement status, the reason it could not be placed, bandwidth and priorities; selecting a node shows the tunnels that are ingress, transit or egress on it, and the placed paths are drawn on the network diagram.
YAML diagrams only, for now
lsps: is available on YAML-based diagrams. Support for tunnels reported
live by a watcher is planned for a later release.
A tunnel in YAML¶
lsps:
TUN_R1_R3: # key = tunnel name
src: 10.10.10.1
dst: 10.10.10.3
metric_type: te # igp (default) | te
bandwidth: 2G # applies to every path unless overridden
setup_priority: 7
admin_groups:
exclude-any: [red]
color: "#ff9900"
autoroute: false # see "autoroute" below
paths: # = LSPs; omit entirely for one dynamic primary
primary:
ero:
- 10.10.10.2 # plain string = loose hop
- {node: 10.10.10.3, hop: strict} # explicit form for a strict hop
secondary:
role: standby
bandwidth: 1G # overrides the tunnel-level default
srlg_exclude: [1001]
Key reference¶
| key | level | values / format | meaning |
|---|---|---|---|
lsps |
top-level | dict, tunnel name → body | optional section next to nodes/edges |
src, dst |
tunnel | node name (IP-address format) | tunnel endpoints |
metric_type |
tunnel | igp (default) | te |
which metric CSPF optimizes for |
bandwidth |
tunnel/path | 2G, 500M, or a raw bps number |
required bandwidth; a path overrides the tunnel default |
setup_priority |
tunnel/path | 0–7, default 7 |
RSVP-TE admission pool (0 is the strongest) |
hold_priority |
tunnel/path | 0–7, defaults to setup_priority |
pool the reservation is held in; may not be weaker than the setup priority |
admin_groups |
tunnel/path | dict: exclude-any / include-any / include-all → list of names |
affinity filter |
srlg_exclude |
path | list of int | SRLG constraint |
color |
tunnel | CSS color | highlight color on the network diagram |
autoroute |
tunnel | bool, default false |
see below |
paths |
tunnel | dict, path name → body; omitted = one dynamic primary |
the tunnel's LSPs |
role |
path | primary (default) | secondary | standby |
path role |
ero |
path | list: 10.10.10.2 (loose hop) or {node: ..., hop: strict} |
explicit route |
A path inherits bandwidth, setup_priority, hold_priority and
admin_groups from the tunnel when it omits them. srlg_exclude and ero are
not inherited — declaring srlg_exclude at tunnel level has no effect, set
it on each path that needs it.
Edges declare the same TE attributes covered on the
Traffic Engineering page
(temetric, max_rsrv_link_bw, admin_group/affinity, srlg, unreserved_bw_0…unreserved_bw_7)
— no separate naming for MPLS purposes.
autoroute¶
A signaled LSP does not redirect traffic on its own — that matches real
RSVP-TE/SR-TE behavior: without autoroute announce (or an explicit static
route pointing at the tunnel), a tunnel is just reserved bandwidth, invisible
to IGP-style path computation. Set autoroute: true on a tunnel to have it
act as a forwarding shortcut in end-to-end path queries (see with_lsps
below) — the real-world equivalent of turning on autoroute on the headend.
Setup and holding priority¶
0 is the strongest priority and 7 the weakest. Leave hold_priority out
and it follows setup_priority, the same as a router's priority <setup>
with the second value omitted.
A tunnel set up at a strong priority but held at a weak one would be
preemptable the moment it is signaled, so that combination is rejected:
hold_priority must be at least as strong as setup_priority
(setup_priority: 0 with hold_priority: 7 is a validation error,
setup_priority: 7 with hold_priority: 0 is fine).
Unsupported (operational) keys¶
rro, oper_status, active_lsp_name, and any label_* key are not
supported in lsps: — they describe live signaling state (Record Route, current
status, active path), not declared intent, and only make sense once a real
watcher reports them. A validation error is raised if you include one.
CSPF path calculation¶
On every recalculation, Topolograph processes each path in setup_priority order
(RSVP-TE convention: 0 highest), same rules a real router would apply:
- filters out edges that don't have enough bandwidth at the requested
setup_prioritypool, don't satisfy the affinity filter, or are in an excluded SRLG; - runs the shortest path over what's left, honoring any
ero(astricthop must be a direct edge from the previous hop — the LSP fails rather than being silently routed around it); - subtracts the placed bandwidth from that pool (and every lower-priority pool) before placing the next path.
Placement never mutates the advertised TE attributes (unreserved_bw_*) — the
consumed capacity is tracked separately, so re-running placement always starts
from the real advertised numbers.
ECMP ties are broken deterministically: fewest hops, then lexicographic order of node names.
Retrieving LSP placement results¶
GET /api/graph/{graph_time}/lsps and GET /api/graph/{graph_time}/lsps/{name}
return each path's placement outcome alongside its declared config:
{
"name": "TUN_R1_R3",
"src": "10.10.10.1",
"dst": "10.10.10.3",
"paths": {
"primary": {
"placed": true,
"reason": null,
"cost": 20,
"path": ["10.10.10.1", "10.10.10.2", "10.10.10.3"]
},
"secondary": {
"placed": false,
"reason": "insufficient bandwidth",
"cost": null,
"path": []
}
}
}
reason explains, in words, why a path could not be placed. Alongside it,
reason_code gives the machine-readable category and binding_constraints
names what is actually blocking:
reason_code |
meaning | what to do |
|---|---|---|
disconnected |
no path exists even with every constraint lifted | repair the topology |
constraints_unsatisfiable |
a path exists, the request is too strict | relax the constraints in binding_constraints (bandwidth, affinity, srlg) |
ero_strict_hop_unreachable |
a strict hop has no edge from the previous hop | fix the ero |
endpoint_not_found |
src/dst is not in the graph |
fix the endpoint |
Several entries in binding_constraints mean lifting any one of them is enough
to place the LSP.
Useful filters on the list endpoint:
# Which tunnels failed to place, and why
graph.lsps_list(status="unplaced")
# Which tunnels cross a given node or link (pre-maintenance impact check)
graph.lsps_list(via_node="10.10.10.2")
graph.lsps_list(via_edge="10.10.10.1,10.10.10.2")
How much TE bandwidth is left on a link, after accounting for every placed tunnel:
graph.edges_list(include=["lsp_left_bw"])
# -> ..., "lsp_left_bw_7": ..., "lsp_reserved_bw": "7Gbps",
# "lsp_left_bw": "3Gbps", "lsp_bandwidth_usage": "7Gbps/3Gbps"
CSPF path, without declaring a tunnel¶
cspf_path answers "which path satisfies these constraints, and at what cost"
— a constraint-filtered shortest-path computation, the same class of query as a
plain shortest path. Nothing is created or persisted:
result = graph.cspf_path(
"10.10.10.1", "10.10.10.7",
bandwidth="5G",
admin_exclude_any=["red"],
)
# {'path': [...], 'cost': 42, 'reason': ''}
# or, if nothing fits: {'path': [], 'cost': None, 'reason': 'no path ... satisfies the requested constraints: ...'}
The answer accounts for bandwidth the already declared tunnels hold: on a
topology with an lsps: section the check runs against what each link has
left after placement, not against an aggregate figure, so it never returns a
path over capacity that is already taken. Constraints are evaluated per setup
priority, so a link can be full at one priority and still have room at a
stronger one.
Affinity constraints (admin_exclude_any, admin_include_any,
admin_include_all) match affinity names on the link. Topologies ingested
from a live network advertise the admin group as a bitmask, so a name-based
include-any/include-all on those graphs matches nothing and the answer is
"no path" — use exclude-any or a YAML topology with named affinities there.
End-to-end path via tunnels (with_lsps)¶
By default, graph.paths.shortest(src, dst) is a plain IGP path — unaffected
by any tunnel in the graph, same as real IP forwarding without autoroute.
Pass with_lsps=True to account for autoroute: true tunnels as forwarding
shortcuts:
graph.paths.shortest("10.10.10.1", "10.10.10.4") # plain IGP path
graph.paths.shortest("10.10.10.1", "10.10.10.4", with_lsps=True) # via active autoroute tunnels
What breaks if a link goes down?¶
edge_failure_reaction predicts the whole-network impact of one or more
links failing — the links that lose traffic and the links that pick it up:
graph.paths.edge_failure_reaction([("10.10.10.1", "10.10.10.2")])
# {'isGraphStillConnected': True, 'affectedLinks': {...}, 'disjointedNodes': []}
For a per-tunnel view of the same question, combine it with lsps_list(via_edge=...)
to see which tunnels traverse the link before checking its failure impact.
Related: YAML-based Topologies · Traffic Engineering attributes · Python SDK