Skip to content

feat(otn): replace the optical schemas with a single OTN extension - #91

Open
iddocohen wants to merge 32 commits into
mainfrom
ic/add-otn-to-schema-v2-2ecax
Open

iddocohen wants to merge 32 commits into
mainfrom
ic/add-otn-to-schema-v2-2ecax

Conversation

@iddocohen

@iddocohen iddocohen commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Replaces extensions/optical_multiplexer and experimental/optical_transport with a single extensions/otn, ported from opsmill/infrahub-demo-otn at e98be9b and reshaped to this library's conventions. 26 kinds out, 43 in: 6 generics and 37 nodes, namespace Otn.

The old two are removed rather than migrated. This is a new model of the domain.

Read this first

This is a starting point, not a production OTN model, in the sense the repo header means. It covers the physical plant, one fixed channel plan, the devices and their ports, carriers and end-to-end services. The omissions below are deliberate and recorded in not_covered, but four of them would matter to an operator, so they are worth a decision rather than a skim.

Two commits are breaking and cannot be loaded over an earlier version of this branch. schema load is additive: deleting an attribute from the YAML does not retire it from a live instance. The Dropdown to Text changes fail with Can only specify 'choices' for kind=Dropdown against a server holding the previous schema. Nothing here has ever been on main, so no consumer is affected, but reviewers testing the branch need a fresh instance rather than an incremental load.

Worth a look

  • LocationSite is reused, extended through an extensions: block. facility and devices become otn_facility and otn_devices, because LocationSite already carries both, the second inherited from LocationHosting. site_type is optional with no default, so loading OTN does not label every site in an estate.
  • Pluggables use the library's transceiver kinds. DcimGenericTransceiver gains otn_port and otn_type. OtnTransceiverType stays here because it references optical modes, so moving it would make extensions/transceiver depend on this extension. transceiver_type gains a dr4 choice: additive, and the only change to a stable extension.
  • OTN ports are cabled with extensions/cable. All nine concrete port kinds inherit DcimEndpoint, so a DcimCable terminates on one and an intra-site jumper is a cable rather than a port attribute. OtnGenericPort.connected_to is gone with it: in the G.872 layering a section owns its adjacency and a port only terminates it, so the cable, OtnFiberSpan.terminating_ports and OtnOpticalPath each answer that at their own layer. A cardinality-one symmetric edge could model neither a 1:N splitter nor a unidirectional element, and it flattened the panel out of a run that physically passes through one.
  • Rack elevations and device types work through dcim_device. OtnGenericDevice.dcim_device reaches a DcimPhysicalDevice carrying position, rack_face, a device_type and a location that may be a LocationRack. The sample data racks both ROADMs, so the route is demonstrated rather than only described. Nothing keeps OtnGenericDevice.site in agreement with the site above that rack, which is stated in not_covered.
  • Nothing is stored that the model already knows. element_class duplicated the node kind and could contradict it, since an OtnMuxDemux was free to declare itself a roadm. OtnFrequencyGrid stored channel_number beside center_frequency_mhz with the formula living only in a description, and the demo data had already drifted, pairing channel 21 with 192.1 THz, which is channel 16 on the 50 GHz plan this extension models. The channel number and OtnCwdmChannel.band are computed now and unwritable, so neither can disagree with what it derives from. Computed attributes are Text or URL only, so the frequency stays the stored Number and keeps its range filter, and the grid's identity moves to it.
  • A multiplex section terminates on any OMS endpoint. OtnOmsEndpoint is implemented by OtnRoadm and OtnMuxDemux, so a fixed point-to-point system built from passive multiplexers is modellable, and with it the amplifier chain and the carrier sections that hang off a section. Before this, roadm_a and roadm_b peered OtnRoadm alone and that whole class of real system had no representation.
  • Each path hop records the cross-connect. A hop carries an action of add, drop, express, through or terminate, plus an optional ingress and egress port. On a ROADM that pair is the cross-connect, which the model could not express: the path knew which elements the light crossed but not which degree or add/drop port it entered on and left by, and add and drop had no representation at all. The inverses sit on OtnGenericPort, so a ROADM's switching state reads from its own ports rather than only by walking every service.
  • Two kinds for the channel plans, G.694.1 and G.694.2, so a carrier on the wrong plan is refused at write time. OtnMuxDemux.plan now says which plan a device is built for, and mux_role gives a fixed OADM a home.
  • Plant lifecycle state and protection paths. OtnFiberSpan, OtnOpticalMultiplexSection and OtnConduit carry status; OtnOpticalPath carries path_role, with uniqueness widened to [[service, segment_sequence, path_role]] so a protect path sits beside its working one. Verified live: the second path is accepted, a duplicate is rejected by the constraint. OtnSplitter models the coupler an optical protection scheme is built from.

Deliberate omissions worth a decision

All are in not_covered and render on the reference page. These four are the ones an optical engineer would ask about.

  • Flexgrid. OtnFrequencyGrid is one fixed 50 GHz plan across the C band, 191.35 to 196.10 THz, 96 channels. L band, C plus L, and the n x 12.5 GHz slot model that 400ZR+ and 800G builds use are out of scope. Supporting them properly needs a plan node above the channels, which is why no per-channel spacing attribute was added.
  • Protection mechanisms. path_role marks a path working or protect and OtnDiversityGroup keeps two services apart, but nothing states the scheme, so 1+1, 1:1, optical line protection, OMS protection and ODU SNCP are indistinguishable. Switching thresholds and hold-off timers go with it.
  • The record of design. OtnOpticalPath records which elements the light crosses, in what order and across which ports, but carries no loss, OSNR margin or latency for that route, so a turn-up figure has nowhere to live. Budgets are computed in a planning tool such as GNPy; the omission is that the result is not stored.
  • The OTU and OPU layers of G.709. The model goes from the optical channel straight to the ODU. FEC lives on OtnOpticalMode, so what an OTU would add is section monitoring and framing overhead.

Also out of scope, and less likely to be contested: optical performance monitoring, since nothing measured belongs here; device types and platforms, reached through dcim_device; and validation a schema cannot express, such as whether a mux client port binds a channel on the plan its multiplexer declares. This extension ships no checks, and the schema comments mark each such rule.

Stacked on #92

This PR targets ic/docs-usecases-and-stale-pages, the branch behind #92, not main. #92 carries the CI housekeeping this branch depends on: it regenerates 15 reference pages that were already stale on main, and it adds the optional use_cases and not_covered keys to docs/_templates/schema_reference.j2 and tasks/docs.py, which OTN's reference page is the only consumer of. Keeping them separate is what holds this diff to 20 files instead of 35. Review #92 first, or at least independently; GitHub retargets this PR to main when #92 merges.

Everything in this diff is the change: extensions/otn, the two optical extensions removed with their objects and pages, .metadata.yml, the OTN sample objects, four stale comments in surviving files, and a dr4 choice in extensions/transceiver.

Verification

Offline on every commit: SDK Pydantic models, peer and inherit resolution, identifier pairing, schema format --check, yamllint, markdownlint, pytest, docs.generate idempotency.

Live, on an instance rebuilt from scratch: all 38 extensions load, every object file loads, and the sample data exercises the new work rather than only describing it. A section runs between two passive multiplexers with no ROADM at either end; the service carries a working path over the north conduit and a protect path over the south, both at segment position 1 and distinguished only by path_role; each hop resolves to its action and port pair from either the path or the ROADM's ports; a two-stage amplifier chain orders by oms_sequence; two CWDM wavelengths derive their band; 192.1 THz derives channel 16 at 1560.61 nm and 193.1 THz derives channel 36 at 1552.52 nm; a DcimCable terminates on an OtnLinePort at one end and an OtnRoadmAddDropPort at the other; each ROADM resolves through dcim_device to a physical record at a rack unit on a LocationRack, carrying a DcimDeviceType; and the wavelength's payload grooms two levels deep, an ODUC4 offering 320 tributary slots into an ODU4 carrying a 100GbE client by GMP and a second ODU4 multiplexing an ODU2 that carries an STM-64 by BMP.

A merged-inheritance collision check written for this branch found its only critical defect, a devices relationship colliding with one inherited two levels up. No existing gate computes a node's merged namespace, so promoting it into CI is worth doing separately.

Gaps

  • Sample objects exercise all 42 relationship identifiers this extension declares. What they do not cover is combinations: one service, one carrier and one channel, so nothing exercises contention between two services on shared plant.
  • Nothing here loads objects/ in CI, so objects files ship unproven by automation. Pre-existing.
  • 12 of 54 library schemas fail schema format --check. None touched here; worth its own PR.
  • Nothing validates that a fitted module's form factor suits its port. The schema marks this and three other rules it cannot express.

Draft so the removal of the two extensions gets a decision first.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Deploying schema-library with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1c4d1b7
Status: ✅  Deploy successful!
Preview URL: https://89a78266.schema-library.pages.dev
Branch Preview URL: https://ic-add-otn-to-schema-v2-2eca.schema-library.pages.dev

View logs

@iddocohen
iddocohen changed the base branch from main to ic/docs-usecases-and-stale-pages September 7, 2026 18:04
@iddocohen iddocohen changed the title feat(otn)!: replace the optical schemas with a single OTN extension feat(otn): replace the optical schemas with a single OTN extension Sep 7, 2026
Both schemas covered the same domain and each warned against loading the
other. They are replaced by extensions/otn, which models the optical
transport network in its own namespace.

BREAKING CHANGE: extensions/optical_multiplexer was a stable extension.
Deployments loading it must migrate to extensions/otn.
The monitor kinds modelled readings, not intended state, and inherited
OtnGenericPort so a device's ports list mixed interfaces with measurement
snapshots. The optical path and hop budget fields were an optical planning
tool's output, mandatory and recomputed whenever a span loss changed. The
service refusal fields were a provisioning workflow no generator here runs.
None of it had a producer in this library.

OtnService.customer now points at OrganizationGeneric rather than carrying a
free-text name.

BREAKING CHANGE: removes OtnMonitor, OtnChannelMonitor, OtnAmplifierMonitor,
OtnRoadmDegreeMonitor, OtnMuxDemuxMonitor, OtnRamanMonitor and
OtnReceiverMonitor, and changes OtnService.customer from an attribute to a
relationship.
Two comments opened by restating the description below them, one cited the
demo's design history, and the inverse-relationship rule sat below the
identifiers it explains. The variable attenuator note pointed its relative
clause at the wrong attribute.
The library declares on_delete only when the answer is cascade, so the
exceptional cases are the ones a reader finds. This file stated the default
65 times, which hid the two relationships that really do cascade.
OtnOpticalPath.service was mandatory but modelled as a cross-reference, so
deleting a service left its paths behind with the link unset. Component and
Parent with cascade makes the ownership explicit, and the delete now reaches
the hops each path already owns.
LocationSite already inherits a devices relationship from LocationHosting,
peering DcimPhysicalDevice, so adding a second one named devices would have
put two GraphQL fields of the same name on one node. It is renamed
otn_devices, matching the otn_facility rename beside it, and keeps its
identifier so the forward side is unchanged.

site_type is no longer mandatory. LocationSite is shared across this
library, and a mandatory OTN field would have declared every site in an
estate a PoP.

The block's attribute order weights move to 1600, 1700 and 1800, clear of
the weights LocationSite already carries, and its relationships are listed
in ascending weight order.

BREAKING CHANGE: LocationSite.devices added by extensions/otn is now
LocationSite.otn_devices.
Six port kinds restated the connector_type Dropdown byte for byte from the
generic they inherit it from, which is the hazard the generic's own comment
warned about. The six remaining Text plus enum attributes become Dropdowns,
so every closed vocabulary in the extension is a Dropdown with labels.

element_class no longer describes an optical budget engine, which this
library does not ship.
…y site is a PoP

The extension had fourteen top-level sidebar entries. Nine kinds now nest
under the five a user actually lands on, matching how patch_panel,
modules_linecards and routing_ospf nest their own kinds.

site_type no longer defaults to pop. LocationSite is shared across this
library, and an optional attribute with a default still applies it on
create, so loading OTN was declaring every site in an estate a PoP. A site
with no stated OTN role now reports nothing.
Three top-level entries instead of five. A pluggable is fitted into a
device port and a carrier realises a service, so both nest where the
relationship already exists. This library ships no menu file, so a single
named OTN heading is not available; nesting is what the automatic sidebar
offers, and depth three is what routing and patch panel already use.
OTN shipped its own transceiver inventory beside extensions/transceiver, so
a user had two places to record one module and the sidebar showed two
Transceiver entries. OtnTransceiver is gone. DcimGenericTransceiver now
carries otn_port, where the module is fitted, because no OTN port is a
DcimEndpoint and interface cannot serve, and otn_type, which part it is.

The part catalogue stays on the OTN side: OtnTransceiverType references
optical modes, so moving it into extensions/transceiver would make that
extension depend on this one. Its vendor Text becomes a relationship to
OrganizationManufacturer, matching the Dcim field.

BREAKING CHANGE: OtnTransceiver is removed. Record an optical pluggable as
a DcimStandardTransceiver with otn_port and otn_type set.
…ratings

The outside plant had no operational state, so a span or a section could not
be marked planned or under maintenance while a device could. A service could
not carry a protect path beside its working one, because OtnOpticalPath had
no role and its uniqueness constraint allowed one path per sequence. Both
were modelled by the extension this branch replaces.

OtnService regains maintenance and failed, OtnOpticalCarrier gains reserved
so a wavelength can be held before it is lit, and OtnAmplifier gains its
nameplate type, stage and output ceiling.

The dense grid's scope is now stated: one 50 GHz C band plan, with L band,
C plus L and flexgrid out of scope.
objects/extensions/dwdm never existed, so that mention is a pre-existing error rather than something this change made stale. It is fixed separately, outside this branch.
Five gaps the removed extensions covered and this one did not. Capacity gives
utilisation a denominator, so occupied channels can be read against a
nameplate. The CDC flags are planning inputs for which wavelength can be
added where. The three dates are the extension's first, and they record
intended state rather than a reading: a commissioning date is entered once,
unlike a measurement, which is why the monitoring exclusion still holds.
Degree direction and the express role restore how operators label a ROADM's
degrees, and external_circuit_id gives a partner's own reference somewhere to
live beside our name for the circuit.
@iddocohen
iddocohen force-pushed the ic/docs-usecases-and-stale-pages branch from b92d55c to 8276299 Compare September 8, 2026 05:20
@iddocohen
iddocohen force-pushed the ic/add-otn-to-schema-v2-2ecax branch from 48e3869 to af60790 Compare September 8, 2026 05:20
Every OTN port kind now inherits DcimEndpoint, so extensions/cable
terminates on optical gear and an intra-site jumper is a DcimCable
rather than a port attribute. OtnGenericPort.connected_to is gone: in
the G.872 layering a section owns its adjacency and a port only
terminates it, so the cable, OtnFiberSpan.terminating_ports and
OtnOpticalPath already answer that question at their own layers. A
cardinality-one symmetric edge could model neither a 1:N splitter nor a
unidirectional element, and it flattened the panel out of a run that
physically passes through one.

Removes two attributes that restated what the graph already held.
element_class duplicated the node kind and could contradict it, since an
OtnMuxDemux was free to declare itself a roadm. OtnFrequencyGrid stored
channel_number beside center_frequency_mhz with the formula living only
in a description, and the demo data had already drifted, pairing channel
21 with 192.1 THz, which is channel 16 on the 50 GHz plan this extension
models. channel_number and OtnCwdmChannel.band are computed now, so
neither can disagree with the quantity it derives from. Computed
attributes are Text or URL only, so the grid's identity moves to
center_frequency_mhz, which stays a Number and keeps its range filter.

An optical multiplex section terminates on the new OtnOmsEndpoint
generic instead of OtnRoadm, so a fixed point-to-point system built from
passive multiplexers is modellable, and with it the amplifier chain and
the carrier sections that hang off a section. OtnRouter becomes an
OtnOpticalElement, so an IPoDWDM path can hop through the routers on its
own ends and a router carries the vendor and model every other device
kind has.

Adds plan, mux_role and technology to OtnMuxDemux, which gives a fixed
OADM a home; technology to OtnRoadm; description to OtnGenericPort; and a
derived centre wavelength to the dense grid.

BREAKING CHANGE: element_class, connected_to and the stored
channel_number are removed, and OtnOpticalMultiplexSection.roadm_a and
roadm_b become endpoint_a and endpoint_b. The Dropdown to Text changes
cannot be applied over an instance carrying the previous schema, which
fails with "Can only specify 'choices' for kind=Dropdown"; load onto a
fresh instance instead.
An OtnPathHop now carries an `action` and an ingress and egress port, so
a hop states what the element does to the carrier and across which port
pair. On a ROADM that pair is the cross-connect, which the model could
not express before: the path knew which elements the light crossed but
not which degree or add/drop port it entered on and left by, and `add`
and `drop` had no representation at all. Both ports are optional, so a
hop through an element whose ports are not modelled still records the
order of travel.

The inverses live on OtnGenericPort as `hops_ingress` and `hops_egress`,
so a ROADM's switching state reads from its own ports rather than only
by walking every service's path. A hop pair is also now unique per path
position, since `path` plus `sequence` is a uniqueness constraint.

Replaces OtnService.service_profile with `service_type` on the
wavelength, transport and IP transit axis. The old dropdown encoded one
deployment's customer taxonomy, which does not belong in a shared
library; `sla` and `max_latency_ns` already carry the commitments and
`client_signal` and `containers` carry the client layer.

Keeps OtnPatchPanel, which an earlier review had proposed removing.
It is the optical face of a frame and the only way a panel can be a
lossy hop on a path, so removing it would lose that. The terminations
belong on the physical side instead, where extensions/patch_panel models
the front and rear interfaces and the mapping between them, and a
comment now says so and says to leave `ports` empty.

BREAKING CHANGE: OtnService.service_profile is replaced by
service_type, whose values are unrelated to the old ones.
The demo data validated a forty-kind model with one straight-line
service: one wavelength, one span, one path. Most of what the model can
express went unexercised, including everything added on this branch. It
now carries a second scenario built from the same two sites.

A far-end ROADM at SJC1, so a wavelength has somewhere to be dropped and
a section has two ends that can terminate it. Two conduits on separate
routes and a second, longer span in the southern one, so diversity is a
property of the plant rather than a label. Three multiplex sections, one
of which runs between two passive coarse multiplexers with no ROADM at
either end, which is the case OtnOmsEndpoint was added for. A two-stage
amplifier chain on the north section, ordered by oms_sequence. A coarse
pair carrying two CWDM wavelengths, whose bands derive from the
wavelength. And the service now has a working path over the north route
and a protect path over the south, both at segment position 1,
distinguished only by path_role, which is what the third term of that
uniqueness constraint is for. Each hop names its action and its port
pair, so both routes read as cross-connect chains.

Adds OtnSplitter and OtnSplitterPort. Optical line protection is built
from a coupler, and the model had no way to express one: element_class
carried a `splitter` value with no matching kind, and removing that
attribute took the last trace of the concept with it. The split loss
stays in insertion_loss_mdb, since a ratio alone does not give it.

OtnAmplifier.oms_sequence becomes optional, matching OtnFiberSpan. Both
oms_a2b and oms_b2a are optional, so an amplifier sitting in no chain
should not have to claim a position in one.

Records four omissions in .metadata.yml that a reader had to discover:
protection mechanisms, the OTU and OPU layers, the absence of a
record-of-design figure on a path, and that device types and rack
elevations are reached through dcim_device rather than modelled natively.
Both ROADMs now carry a physical record through dcim_device: a DcimDevice
at a rack unit on a LocationRack, with a DcimDeviceType. The extension
documents that route as how device types and rack elevations are reached,
and nothing in the sample data demonstrated it. LocationRack and
DcimDeviceType are created here rather than added to this extension's
dependencies, which is what nine other extensions' object files already
do for LocationRack.

The sample also labelled every optical device Cisco. Cisco does build
ROADMs, in the NCS 2000 line, but an all-one-vendor network reads as
placeholder data. The line system, transponders, amplifiers and dense
multiplexer are Ciena, the passive coarse pair and the protection coupler
are Adtran, and Cisco keeps the grey client pluggable, where a
third-party optic is ordinary. Names stay at platform granularity rather
than inventing card codes, and the dense multiplexer's channel_capacity
now matches the CMD44 it names.
The ODU and container layer was the part of the model that nothing in the
sample data touched, which left the deepest part of G.709 described but
never loaded. The 400G wavelength now carries an ODUC4 offering 320
tributary slots, with one ODU4 taking a 100GbE client directly by GMP and
a second ODU4 multiplexing a lower-order ODU2 that carries an STM-64 by
BMP. Slot counts are the G.709 figures the schema descriptions cite: an
ODU4 takes 80 slots in an ODUC4 and offers 80 to its own children, an
ODU2 takes 8 of those. Parent and child edges are exercised two levels
deep, which is what the directional self-relationship on OtnContainer is
for.

The service becomes `transport` and names its headline client signal. A
service whose payload is groomed into containers is not a whole lambda
handed over, so `wavelength` contradicted its own data.

Also fills the last of the untouched relationships: the carrier names its
optical mode and both sections it traverses, the north section gains its
return-direction amplifier chain on `oms_b2a`, a distributed Raman pump
sits on the long south span, an ODU cross-connect switches the carrier at
the far end, and a coherent 400ZR pluggable joins the catalogue so a
transceiver type has a mode to declare. Two optical modes carry the
vendor figures for a transponder line side and for 400ZR.

Sample data now exercises all 42 relationship identifiers this extension
declares, up from 30. The client signal catalogue loads ahead of the
service that references it, since object specs resolve in file order.
@iddocohen
iddocohen marked this pull request as ready for review September 8, 2026 10:33
Base automatically changed from ic/docs-usecases-and-stale-pages to main September 8, 2026 18:04
@iddocohen
iddocohen requested review from a team and minitriga September 8, 2026 18:04
@BaptisteGi

Copy link
Copy Markdown
Contributor

Not familiar enough with the use case to have a strong opinion, but that does seem pretty extensive.

I do have two remarks:

  • I can see users having a couple of optical multiplexers to connect sites/racks (DWDM, etc.), without necessarily running a full optical network and therefore needing the full-blown OTN extension. So I’m wondering if we could extract this into a dedicated extension and mark it as a prerequisite for OTN.
  • As a follow-up to the first point, perhaps there are other constructs in there that could also be meaningful to extract into dedicated extensions. I tend to think having multiple extensions would make the schema easier to maintain, as well as easier for users to consume. In the end, OTN could be a collection of schemas in the marketplace. Someone running an OTN environment and getting started with Infrahub could simply get that collection from the marketplace and be in a good spot.

The second point doesn’t need to be solved now; the first one might be more important. Curious to hear others’ opinions.

@iddocohen

Copy link
Copy Markdown
Contributor Author

Starting from the multiplexer kinds and following only inheritance and mandatory relationships, the closure is 10 kinds, and 4 of the 5 additional kinds are abstract generics (OtnGenericDevice, OtnGenericPort, OtnOpticalPort, OtnOpticalElement).

So your instinct is right that this is separable; however, the blocker is that inherit_from is immutable once a node is defined. So whichever file defines OtnMuxDemux has to declare its full ancestry at that point. A standalone mux extension would therefore have to declare OtnOmsEndpoint and OtnOpticalElement itself, or OTN loses the ability to terminate a multiplex section on a passive mux, which is this PR's main structural fix.

So maybe the approach should be: an otn_core of generics with otn_multiplexer (5 kinds), otn_plant (16) and otn_service (16) as peers on top, published as a marketplace collection (going to your second point). That is ok but I would rather do that as a follow-up (if we feel this is needed).

One note on the consumption concern: OTN adds 43 kinds but only 3 top-level sidebar entries, so someone with a couple of muxes sees Devices, Fiber span and Service rather than a wall.

…v2-2ecax

# Conflicts:
#	.metadata.yml
#	docs/docs/home.mdx
@BeArchiTek
BeArchiTek removed the request for review from a team September 11, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants