Quick start
Go from a fresh clone to a proposed change Infrahub will not merge. You provision a 400G service from Berlin to Amsterdam, then ask the same network for a circuit it cannot carry and watch a check hold the merge shut.
Nine and a half minutes of command time, measured on a warm machine with the
images already pulled. invoke init is eight of them, and the rest is seconds.
Registration keeps running for a few minutes after init returns, so the
proposed change at the end waits on that rather than on you.
Before you start​
You need Docker with Compose v2, uv, and about 8 GB of free memory for the stack. Install and load the demo carries the full prerequisites, what each step loads and how to verify it. This page runs a subset of those commands and explains less.
The first command below runs invoke init, which takes the containers down with
-v and deletes their volumes. Every object in the current stack goes with them,
on every branch, and nothing is exported first. Run it on a machine whose
Infrahub data you can afford to lose.
When a step fails​
Every step here is an invoke task, and the tasks read the address and the API
token from .env and pass both to whatever they run. Install and load the
demo is where the failures are covered: the check
that says whether the shared package reached the worker, the Authentication failure that appears when a tool reads the address from .env but not the
token, and how to tear the stack down and start over.
What is deliberately red on the default branch​
The OSNR check fails on the default branch, and that is the shipped state.
Paris to Madrid is 1250 km and falls 0.535 dB short at DP-16QAM 64GBd 400G,
in both directions. It is a true finding about a long route rather than a
broken demo, and you will meet it on every branch cut from the default one,
including both branches below. The link
budget
page has the answers an engineer would reach for and what each one costs.
Start the stack​
git clone https://github.com/opsmill/infrahub-demo-otn
cd infrahub-demo-otn
cp .env.example .env
uv sync
uv run invoke init
invoke init destroys any existing stack, starts a new one, loads the schema,
the menu and the dataset, and registers this repository. Registration is the
step that turns the proposed-change pipeline on, so the checks below have
something to run in.
Infrahub is then at http://localhost:8000.

What registration bought you. Every PoP now carries two maps as artifacts, rendered from the graph rather than drawn by hand. This is Berlin's copy.
Provision Berlin to Amsterdam at 400G​
uv run invoke demo-provision
It does not ask you for a branch. It makes the demo branch if it is not there,
in Git as well as in the graph, and that second half is what lets a proposed
change from it read the checks, the generator and the artifact definitions out of
this repository. A branch that exists only in the graph runs the two built-in
validators and stops.
Nor does it reload the network. An Infrahub branch inherits the data on the
branch it forks from, and invoke init put the dataset on the default branch, so
demo has the whole network the moment it exists. What it does load is the five
service requests, which are scenario input rather than plant. It says so:
- branch demo does not exist yet, creating it in Git and in the graph
- the dataset is already there, branch demo is ready
- loading the 2 scenario file(s) branch demo needs
Each request is intent and nothing else: svc-ber-ams-400g asks for 400 Gbps
between two routers and names no route, no wavelength and no channel.
The generator then runs. It budgets six candidate routes at every transponder mode that reaches 400 Gbps and takes the narrowest mode that closes:
svc-ber-ams-400g: chose oms-ham-ber|oms-ams-ham on DP-16QAM 64GBd 400G, channel 2,
800 km, margin +2.284 dB, 3923.026 us
That run wrote 29 objects, among them one carrier, one optical path, 25 hops in
order and two containers, and moved the service to active. Scenario
one
walks all six candidates, including why the winning route anchors on channel 2
rather than channel 1.

The service after the generator ran. It arrived naming no route and now has an optical path and a container, with both rejection fields empty.
Open a proposed change from demo and the pipeline runs nine checks, the
generator and three artifact definitions, reporting each on the change. No
service on the branch is refused, so provisionable passes. osnr_margin is
still red on the Paris to Madrid deficit, as it is on every branch. That red is
the shipped finding above, not anything this branch did.
The refusal that blocks the merge​
uv run invoke demo-regenerator
Madrid to Warsaw is 2970 km over four sections, and no single wavelength closes
it on any of the ten modes in the catalog. The task makes oeo-refused, loads a
file that puts a regenerator at each of the three sites on that route where one
could go, and asks for the service. All three splits are refused for optical
budget, and one run reports all three. It then runs the check, which reads the
refusal off the service:
svc-mad-waw-400g cannot be provisioned and was refused for budget: neither a
direct wavelength nor a chain serves this route. Direct:
oms-par-mad|oms-par-fra|oms-prg-fra|oms-prg-waw closes on no mode: best margin
-0.021 dB on DP-QPSK 128GBd 400G. Chain:
oc-ch070-mad-par|oeo-par-01|oc-ch070-par-waw does not close: segment 1 -0.535
dB, segment 2 -2.439 dB. It has not been accepted, so this branch does not
merge. Fix the network or the request, or set refusal_accepted on the service to
keep the refusal on the record
Open a proposed change from oeo-refused and that sentence is what a reviewer
sees. A check that logs an error means the change cannot be merged. log_error
blocks and log_info annotates, and there is no log_warning in between to
soften a refusal with. The branch merges once somebody fixes the route or sets
refusal_accepted on the service, which puts the decision on the record rather
than passing quietly.
provisionable is registered with no targets, which makes it global: it judges
every service on the branch rather than the one you edited. A service nobody
touched can be the one that blocks your merge.
Two other checks are red on this branch, and neither of them is the point.
osnr_margin fails on the Paris to Madrid deficit that sits on every branch,
and channel_collision fails because the three regenerator wavelength pairs
this file loads overlap in spectrum on purpose.
The command does not stop there. It goes on to make a second branch,
oeo-closed, adds one more wavelength pair at a lower modulation order and asks
for the same service again. That one provisions as two segments and
provisionable passes, so a single run shows you the refusal and the fix for it.
Loadable scenarios
has the figures for both halves.

The same fields as the provisioned service, filled in the other direction. The last row is the one that matters: nobody has accepted this refusal, so it blocks.

The merge held shut. Sixteen validators ran; provisionable is the one this
page is about. osnr_margin is the shipped Paris to Madrid deficit, and
channel_collision fires because this scenario overlaps three regenerator
pairs in spectrum on purpose.
Clean up​
uv run invoke demo-clean
With no argument it deletes every branch a scenario task creates, naming each one as it goes and reporting how many of them it found, so nothing is removed without saying so. That is all three branches this page made.
The default branch is back to the state it shipped in: the plant, no services, and the OSNR check still failing on Paris to Madrid. Every service you provisioned lived on a branch, which is the point.
Where to go next​
- What this demo shows of Infrahub lists each Infrahub capability the demo exercises, names the scenario that shows it, and says plainly which capabilities the demo does not touch.
- Provisioning scenarios walks the Berlin to Amsterdam run in full, then puts a second service on a corridor that has run out of spectrum and shows why one closes and the other does not.
- Loadable scenarios and the runbook carries the fix for Madrid to Warsaw: the same route closes as two segments once one more wavelength pair arrives at a lower modulation order.