Mix.install(
[
{:diffo_example, "~> 0.2.3"},
{:diffo, "~> 0.4.1"},
{:kino, "~> 0.14"}
],
config: [
bolty: [
{Bolt,
[
uri: "bolt://localhost:7687",
auth: [username: "neo4j", password: "password"],
user_agent: "diffoExampleNbnLivebook/1",
pool_size: 15,
max_overflow: 3,
prefix: :default,
name: Bolt,
log: false,
log_hex: false
]}
]
],
consolidate_protocols: false
)Overview
NBN is a fictional, simplified take on Australia's wholesale broadband network. Where Access is a single telco with its own DSL service, NBN is wholesale — one physical network shared by many Retail Service Providers (RSPs).
This notebook walks the provisioning flow when an RSP sells an NBN Ethernet access to a subscriber. Two things make NBN richer than Access:
- Multi-tenancy — every resource is owned by an RSP and policy-scoped to its owner.
- Longer delivery chain —
NbnEthernet(PRI) owns anAvcand aUni. TheAvcconsumes a:cvlanfrom aCvc. TheCvcconsumes an:svlanfrom anNniGroup. TheUniconsumes a:portfrom anNtd. Every step is a chance to bring upstream context up.
See nbn.md for the narrative version including the named-vs-metrics characteristic pattern, the alias convention, and what each consumer inherits.
Setting up
Connect to Neo4j (running locally on the default port). It is helpful to keep the Neo4j browser open at http://localhost:7474/browser/ as you go through the cells.
AshNeo4j.BoltyHelper.is_connected()Optional — clear the database so the scenario builds from a clean slate:
AshNeo4j.Neo4jHelper.delete_all()alias Diffo.Provider.Assignment
alias Diffo.Provider.Instance.Relationship
alias DiffoExample.Nbn
alias DiffoExample.Nbn.{CvcMetrics, NniGroupMetrics}The Retail Service Providers
Seed the RSPs and pick one to operate as. Every resource we build will be owned by that RSP and isolated from resources owned by others.
DiffoExample.Nbn.Initializer.init()
rsps = Nbn.list_rsps!()
Kino.DataTable.new(rsps, keys: [:id, :short_name, :name, :state])rsp_input =
Kino.Input.select(
"Operate as RSP",
Enum.map(rsps, fn rsp -> {rsp.name, Atom.to_string(rsp.short_name)} end)
)actor = Enum.find(rsps, fn rsp -> Atom.to_string(rsp.short_name) == Kino.Input.read(rsp_input) end)
actor1. Shareable infrastructure — NNI Group + NNIs
An RSP builds (or has built for it) shareable infrastructure at each Point of Interconnect: an NniGroup containing Nnis, and a Cvc that terminates at the NNI Group. This gets done once per POI per RSP and serves many customers.
{:ok, nni_group} = Nbn.build_nni_group(%{}, actor: actor)
{:ok, nni_group} =
Nbn.define_nni_group(
nni_group,
%{
characteristic_value_updates: [
nni_group: [group_name: "SYD-POI-01", location: "Sydney Olympic Park"],
svlans: [first: 1, last: 4000, assignable_type: "svlan"]
]
},
actor: actor
)Add two Nnis (the physical interconnect ports) and relate them as :contains:
nni_ids =
for {port_id, capacity} <- [{"SYD-01-ETH-1", 10_000}, {"SYD-01-ETH-2", 10_000}] do
{:ok, nni} = Nbn.build_nni(%{}, actor: actor)
{:ok, _} =
Nbn.define_nni(
nni,
%{
characteristic_value_updates: [
nni: [port_id: port_id, capacity: capacity]
]
},
actor: actor
)
nni.id
end
{:ok, nni_group} =
Nbn.relate_nni_group(
nni_group,
%{
relationships:
Enum.map(nni_ids, fn id ->
%Relationship{id: id, direction: :forward, type: :contains}
end)
},
actor: actor
)2. Shareable infrastructure — CVC
A Cvc takes an :svlan from the NniGroup and names its upstream :nni_group (the consumer-alias names the related resource):
{:ok, cvc} = Nbn.build_cvc(%{}, actor: actor)
{:ok, cvc} =
Nbn.define_cvc(
cvc,
%{
characteristic_value_updates: [
cvc: [bandwidth: 1000],
cvlans: [first: 1, last: 4000, assignable_type: "cvlan"]
]
},
actor: actor
)
{:ok, _nni_group} =
Nbn.assign_svlan(
nni_group,
%{
assignment: %Assignment{
assignee_id: cvc.id,
alias: :nni_group,
operation: :auto_assign
}
},
actor: actor
)3. Per-customer infrastructure — NTD + UNI
The Ntd is the device installed at the customer premises — NBN-managed, no RSP actor: needed. The Uni consumes a :port from it and names its upstream :ntd.
{:ok, ntd} = Nbn.build_ntd(%{})
{:ok, ntd} =
Nbn.define_ntd(ntd, %{
characteristic_value_updates: [
ntd: [model: "Sercomm CG4000A", serial_number: "SCOMA1A057A2", technology: :FTTP],
ports: [first: 1, last: 4, assignable_type: "port"]
]
})
{:ok, uni} = Nbn.build_uni(%{})
{:ok, uni} =
Nbn.define_uni(uni, %{
characteristic_value_updates: [
uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP]
]
})
{:ok, _ntd} =
Nbn.assign_port(ntd, %{
assignment: %Assignment{
assignee_id: uni.id,
alias: :ntd,
operation: :auto_assign
}
})4. The AVC
An Avc belongs to one RSP. It consumes a :cvlan from the RSP's Cvc and names its upstream :cvc:
{:ok, avc} = Nbn.build_avc(%{}, actor: actor)
{:ok, avc} =
Nbn.define_avc(
avc,
%{
characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]]
},
actor: actor
)
{:ok, _cvc} =
Nbn.assign_cvlan(
cvc,
%{
assignment: %Assignment{
assignee_id: avc.id,
alias: :cvc,
operation: :auto_assign
}
},
actor: actor
)5. The NBN Ethernet access (PRI)
The PRI is the service the RSP sells. It owns the Avc and the Uni via two :owns relationships — aliased :circuit (the role the AVC plays — the access virtual circuit) and :port (the role the UNI plays — the customer's port):
{:ok, pri} = Nbn.build_nbn_ethernet(%{}, actor: actor)
{:ok, _pri} =
Nbn.relate_nbn_ethernet(
pri,
%{
relationships: [
%Relationship{id: avc.id, direction: :forward, type: :owns, alias: :circuit},
%Relationship{id: uni.id, direction: :forward, type: :owns, alias: :port}
]
},
actor: actor
)6. Inheritance — the brought-up delivery chain
Load the PRI with all four inherited characteristics. Each one resolves through the assignment and relationship graph live:
{:ok, pri} =
Nbn.get_nbn_ethernet_by_id(
pri.id,
load: [:avc, :uni, :cvc, :ntd],
actor: actor
)
%{
avc: pri.avc,
uni: pri.uni,
cvc: pri.cvc,
ntd: pri.ntd
}Single-hop (:avc, :uni) goes via the :circuit and :port owns relationships. Two-hop (:cvc, :ntd) walks the relationship hop then back through the :cvc and :ntd assignment aliases. All singular — the AssignmentRelationship's [target_id, alias] identity guarantees at most one upstream per hop.
The AVC and CVC can also bring up their own context:
{:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:cvc, :nni_group], actor: actor)
%{
cvc: avc.cvc, # single-hop via :cvc
nni_group: avc.nni_group # two-hop via [:cvc, :nni_group]
}7. Metrics — the local KPIs
Metrics are local-only characteristics that don't propagate. Read the CVC's metrics and the NNI Group's metrics directly:
cvc_metrics =
CvcMetrics
|> Ash.Query.filter_input(instance_id: cvc.id)
|> Ash.Query.load(:value)
|> Ash.read_one!(actor: actor)
cvc_metrics.valuenni_group_metrics =
NniGroupMetrics
|> Ash.Query.filter_input(instance_id: nni_group.id)
|> Ash.Query.load(:value)
|> Ash.read_one!(actor: actor)
nni_group_metrics.valueutilization = cvcs_total_bandwidth / nnis_total_bandwidth — demand over capacity at the NNI Group. Expected 0–1 under normal provisioning; >1 under deliberate oversubscription.
NniGroup.nnis brings up the typed values of each contained NNI (low-cardinality, so the list is fine):
{:ok, nni_group} = Nbn.get_nni_group_by_id(nni_group.id, load: [:nnis], actor: actor)
nni_group.nnis8. TMF JSON
The PRI serialises to TMF-shaped JSON. The metrics characteristic is inline; the :owns relationships surface in resourceRelationship; the typed characteristics are surfaced post-#169.
pri
|> Jason.encode!()
|> Jason.decode!()
|> Jason.encode!(pretty: true)
|> IO.puts()The NNI Group's JSON shows the :contains relationships to NNIs alongside the :assignedTo relationships to CVCs, the svlans pool, and the metrics characteristic:
nni_group
|> Jason.encode!()
|> Jason.decode!()
|> Jason.encode!(pretty: true)
|> IO.puts()Exploring the graph
MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 100;You'll see Specification nodes (one per resource type), Instance nodes (the things we just built), Characteristic nodes (typed and pool), and the assignment / relationship edges between them. Filter by the RSP's stamp to see one tenant's slice.
What next?
You've used multi-tenancy, the long delivery chain, the named-vs-metrics pattern, the alias convention, and the cross-resource inheritance — every pattern from the #49 design.
Open provider.md if you want to revisit the primitives now that you've seen them at scale, or access.md for the simpler single-tenant warm-up.
When you're ready to model your own domain, start with one specification, declare its characteristics, decide whether anything pools, and watch the JSON come out the other side. The structure carries you a long way.