Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ diffo-*.tar

/.elixir_ls

.DS_Store
.DS_Store

# Agent related
.claude/*
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
### Fixes:
* fixed relationship enrichment inconsistent across neo4j versions

## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.0.4..v0.2.0) (2026-04-24)
## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.0.4..v0.2.0) (2026-04-26)

### Maintenance:
* updated to diffo 0.2.0

### Features:
* new NBN domain modelling NBN Ethernet access and constituent resources (UNI, AVC, NTD, CVC, NNI Group, NNI)
* NBN Technology and Speeds as Ash Enum types
* speeds derived from NTD technology and AVC bandwidth_profile via mine action
* JSON API via AshJsonApi and Plug.Cowboy
* RSP resource with AshStateMachine lifecycle (inactive/active/suspended) and Ash Policy authorisation
* RSP multi-tenancy: SetRspId change, OwnedByActor and NoActor policy checks, RspOwnership macro shared across RSP-owned resources
* NTD and UNI modelled as NBN-owned infrastructure — readable by any RSP, mutable only by internal calls
* Interactive NBN livebook with Kino RSP selector and actor-scoped provisioning flow
* NBN domain documentation including Perentie ecosystem narrative
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ SPDX-License-Identifier: MIT

[![Module Version](https://img.shields.io/hexpm/v/diffo)](https://hex.pm/packages/diffo_example)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen)](https://hexdocs.pm/diffo_example/)
[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo_example%2Fblob%2Fmain%2Fdiffo_example.livemd)
[![License](https://img.shields.io/hexpm/l/diffo)](https://github.com/diffo-dev/diffo_example/blob/master/LICENSES/MIT.md)
[![REUSE status](https://api.reuse.software/badge/github.com/diffo-dev/diffo_example)](https://api.reuse.software/info/github.com/diffo-dev/diffo_example)

This repo contains Diffo Examples.

[Diffo](https://github.com/diffo-dev/diffo) is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks.

This repo contains two independent example domains, each modelling a different slice of a telco network.

## NBN Domain

A declarative model of a fictional NBN Ethernet access hierarchy — NbnEthernet, UNI, AVC, NTD, CVC, NNI Group, and NNI — built entirely with the Diffo Provider Instance DSL. Includes multi-tenancy via Ash Policy: each RSP can only see and manage the resources they own.

[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo_example%2Fblob%2Fdev%2Fdocumentation%2Fdomains%2Fdiffo_example_nbn.livemd)

The livebook walks through provisioning a complete NBN Ethernet access circuit, selecting an RSP to operate as, and demonstrating how the `mine` actions propagate technology, speeds, CVLAN, and port assignments up the resource hierarchy.

## Access Domain

A copper-network equivalent covering DSL access services — Cable, Card, Path, and Shelf. Explore `lib/access/` for the domain model.

## Installation

Expand All @@ -32,13 +42,6 @@ end

You need [Neo4j](https://github.com/neo4j/neo4j) available. We recommend the Neo4j Community 5 latest, available at [Neo4j Deploymnent Centre](https://neo4j.com/deployment-center/) which can be installed locally. You can also configure connection to a cloud based database service such as [Neo4j AuraDB](https://neo4j.com/product/auradb/).

## Tutorial

Click the **Run in Livebook** badge above to open the interactive tutorial, or find it at [diffo_example.livemd](diffo_example.livemd).

The diffo_example livebook walks through provisioning a complete NBN Ethernet access circuit — NTD, UNI, AVC, CVC, NNI Group, and NNI — showing how the `mine` actions propagate technology, speeds, CVLAN, and port assignments up the resource hierarchy.


## Contributions

Contributions are welcome, please start with an [issue](https://github.com/diffo-dev/diffo_example/issues)
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ config :spark,
:characteristics,
:neo4j,
:jason,
:json_api,
:outstanding,
:actions,
:state_machine,
Expand Down
175 changes: 109 additions & 66 deletions diffo_example.livemd → ...entation/domains/diffo_example_nbn.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@ SPDX-License-Identifier: MIT
```elixir
Mix.install(
[
{:diffo_example, "~> 0.2.0"}
{:diffo_example, "~> 0.2.0"},
{:kino, "~> 0.14"},
{:req, "~> 0.5"}
],
config: [
bolty: [
{Bolt,
[
uri: "bolt://localhost:7687",
auth: [username: "neo4j", password: "password"],
user_agent: "diffoExampleLivebook/1",
pool_size: 15,
max_overflow: 3,
prefix: :default,
name: Bolt,
log: false,
log_hex: false
]}
]
],
consolidate_protocols: false
)
Expand Down Expand Up @@ -37,25 +55,9 @@ The NBN domain models a fictional NBN Ethernet access circuit and its constituen

## Installing Neo4j and Configuring Bolty

Update the configuration below to match your Neo4j installation and evaluate.
Bolty is configured in the `Mix.install` block above — update the Neo4j credentials there if needed before evaluating.

```elixir
config = [
uri: "bolt://localhost:7687",
auth: [username: "neo4j", password: "password"],
user_agent: "diffoExampleLivebook/1",
pool_size: 15,
max_overflow: 3,
prefix: :default,
name: Bolt,
log: false,
log_hex: false
]
```

```elixir
AshNeo4j.BoltyHelper.start(config)
```
You need [Neo4j](https://neo4j.com/deployment-center/) installed and running. Verify the connection:

```elixir
AshNeo4j.BoltyHelper.is_connected()
Expand All @@ -69,42 +71,28 @@ It is helpful to have a Neo4j browser open locally, typically at http://localhos
AshNeo4j.Neo4jHelper.delete_all()
```

## Setup Aliases

```elixir
require Ash.Query
alias DiffoExample.Nbn
alias DiffoExample.Nbn.NbnEthernet
alias DiffoExample.Nbn.Uni
alias DiffoExample.Nbn.Avc
alias DiffoExample.Nbn.Ntd
alias DiffoExample.Nbn.Cvc
alias DiffoExample.Nbn.NniGroup
alias DiffoExample.Nbn.Nni
alias DiffoExample.Nbn.Technology
alias DiffoExample.Nbn.Speeds
import Jason, only: [encode: 2]
```

## About NBN
## About NBN Co

NBN (National Broadband Network) is Australia's wholesale fixed-line access network, operated by NBN Co. It provides standardised access products to Retail Service Providers (RSPs), who in turn deliver internet and other services to end customers.

For the purpose of this example we are going to refer to a simplified, and re-imagined NBN Co as NBN.

An RSP typically combines:

* An **NBN Ethernet** access circuit (UNI + AVC) at the customer premises — the access and aggregation layer modelled in this domain
* A **home gateway** device installed at the UNI, which provides the customer's LAN, Wi-Fi, and sometimes voice
* Transport, aggregation, and edge infrastructure connecting the NNI to the RSP's network and on to the internet

NBN Co connects the customer premises to the RSP's network via a Point of Interconnect (POI). The NNI sits at the POI, grouped into NNI Groups. AVCs carrying customer traffic are aggregated onto a CVC, which terminates at the NNI Group. The RSP purchases CVC capacity to carry the aggregate traffic of its customers at that POI.
NBN connects the customer premises to the RSP's network via a Point of Interconnect (POI). The NNI sits at the POI, grouped into NNI Groups. AVCs carrying customer traffic are aggregated onto a CVC, which terminates at the NNI Group. The RSP purchases CVC capacity to carry the aggregate traffic of its customers at that POI.

NBN is delivered over several access technologies — FTTP, FTTN, FTTB, FTTC, HFC, Fixed Wireless, and Satellite — which determine which bandwidth profiles and speeds are available to a given premises.
NBN delivers over several access technologies — FTTP, FTTN, FTTB, FTTC, HFC, Fixed Wireless, and Satellite — which determine which bandwidth profiles and speeds are available to a given premises.

## Technology and Speeds
## NBN Ethernet Technology and Speeds

The NBN domain defines Technology as an Ash Enum covering all NBN access types:

```elixir
alias DiffoExample.Nbn.{Technology,Speeds}
Technology.values()
```

Expand All @@ -127,53 +115,86 @@ Speeds.speeds(:wireless_superfast, :FixedWireless)
Speeds.speeds(:home_fast, :FixedWireless)
```

## Building the Network Hinterland
## Multi-tenancy

Each RSP operates in isolation — they can only see and manage the resources they own. This multi-tenancy is enforced at the Ash policy layer: every NBN resource is stamped with the owning RSP's id at creation, and subsequent reads, updates, and destroys are scoped to the record owner.

Select the RSP you want to operate as for the rest of this livebook. All resources you build will be owned by that RSP and isolated from resources owned by others.

```elixir
alias DiffoExample.Nbn
alias DiffoExample.Nbn.Rsp
import Jason, only: [encode: 2]
DiffoExample.Nbn.Initializer.init()
rsps = Nbn.list_rsps!()
Kino.DataTable.new(rsps, keys: [:epid, :name, :short_name, :state])
```

```elixir
rsp_input = Kino.Input.select(
"Operate as RSP",
Enum.map(rsps, fn rsp -> {rsp.name, Atom.to_string(rsp.short_name)} end)
)
```

```elixir
actor = Enum.find(rsps, fn rsp -> rsp.name == Kino.Input.read(rsp_input) end)
actor
```

## Maintaining Shareable Resources

As an RSP we need maintain some shareable network resources: NNI, NNI Group, and CVC.

Before we can provision an NBN Ethernet access we need the shared network resources: NNI, NNI Group, and CVC.
We'll need these everywhere we operate, in advance of and sufficient for all the NBN Ethernet Accesses we have. We'll just build one of each right now.

Build an NNI — the physical interconnect between the RSP and NBN Co:
Build an NNI — the physical interconnect between the RSP and NBN:

```elixir
nni = Nbn.build_nni!(%{})
alias DiffoExample.Nbn.{Nni, NniGroup, CVC}
nni = Nbn.build_nni!(%{}, actor: actor)
nni |> Jason.encode!(pretty: true) |> IO.puts
```

Build an NNI Group — a logical grouping of NNIs at a point of interconnect:

```elixir
nni_group = Nbn.build_nni_group!(%{})
nni_group = Nbn.build_nni_group!(%{}, actor: actor)
nni_group |> Jason.encode!(pretty: true) |> IO.puts
```

Define the NNI Group with an SVLAN assignment and relate the NNI:

```elixir
nni_group = Nbn.define_nni_group!(%{
nni_group = Nbn.define_nni_group!(nni_group, %{
characteristic_value_updates: [nni_group: [svlan: 100]]
})
}, actor: actor)
nni_group = Nbn.relate_nni_group!(nni_group, %{
relationships: [%{alias: :nni, target_id: nni.id, type: :isAssigned}]
})
relationships: [%Diffo.Provider.Instance.Relationship{id: nni.id, alias: :nni, type: :isAssigned}]
}, actor: actor)
nni_group |> Jason.encode!(pretty: true) |> IO.puts
```

Build a CVC — the aggregation virtual circuit that terminates at the NNI Group:

```elixir
cvc = Nbn.build_cvc!(%{})
cvc = Nbn.build_cvc!(%{}, actor: actor)
cvc = Nbn.relate_cvc!(cvc, %{
relationships: [%{alias: :nni_group, target_id: nni_group.id, type: :isAssigned}]
})
relationships: [%Diffo.Provider.Instance.Relationship{id: nni_group.id, alias: :nni_group, type: :isAssigned}]
}, actor: actor)
cvc |> Jason.encode!(pretty: true) |> IO.puts
```

## Provisioning an NBN Ethernet Access
## Provisioning NBN Ethernet

With the hinterland in place we can provision a customer-facing NBN Ethernet access.
For each customer site we want to provide service to, we need an NBN Ethernet composite resource, involving an NTD, UNI, AVC and CVC.

The NTD is NBN infrastructure — built and managed by NBN, visible to any RSP. It may not exist at a new or existing customer site, so may be built on demand by NBN.

Build an NTD — the device installed at the customer premises:

```elixir
alias DiffoExample.Nbn.{Ntd, Uni, Avc, NbnEthernet}
ntd = Nbn.build_ntd!(%{})
ntd = Nbn.define_ntd!(ntd, %{
characteristic_value_updates: [ntd: [technology: :FTTP, ports: [1, 2, 3, 4]]]
Expand All @@ -187,7 +208,7 @@ Build a UNI — the interface at the customer premises — and assign a port fro
uni = Nbn.build_uni!(%{})
alias Diffo.Provider.Assignment
ntd = Nbn.assign_port!(ntd, %{
assignment: %Assignment{assignee_id: uni.id, value: 1}
assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign}
})
ntd |> Jason.encode!(pretty: true) |> IO.puts
```
Expand All @@ -196,7 +217,7 @@ Relate the UNI back to the NTD so it can mine technology and port from it:

```elixir
uni = Nbn.relate_uni!(uni, %{
relationships: [%{alias: :ntd, target_id: ntd.id, type: :isAssigned}]
relationships: [%Diffo.Provider.Instance.Relationship{id: ntd.id, alias: :ntd, type: :isAssigned}]
})
uni = Nbn.mine_uni!(uni, %{})
uni |> Jason.encode!(pretty: true) |> IO.puts
Expand All @@ -205,28 +226,28 @@ uni |> Jason.encode!(pretty: true) |> IO.puts
Build an AVC and assign it a CVLAN from the CVC:

```elixir
avc = Nbn.build_avc!(%{})
avc = Nbn.build_avc!(%{}, actor: actor)
avc = Nbn.define_avc!(avc, %{
characteristic_value_updates: [avc: [bandwidth_profile: :home_ultrafast]]
})
}, actor: actor)
cvc = Nbn.assign_cvlan!(cvc, %{
assignment: %Assignment{assignee_id: avc.id, value: 200}
})
avc = Nbn.mine_avc!(avc, %{})
assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign}
}, actor: actor)
avc = Nbn.mine_avc!(avc, %{}, actor: actor)
avc |> Jason.encode!(pretty: true) |> IO.puts
```

Now build the top-level NBN Ethernet access and relate it to both the UNI and AVC:

```elixir
pri = Nbn.build_nbn_ethernet!(%{})
pri = Nbn.build_nbn_ethernet!(%{}, actor: actor)
pri = Nbn.relate_nbn_ethernet!(pri, %{
relationships: [
%{alias: :uni, target_id: uni.id, type: :isAssigned},
%{alias: :avc, target_id: avc.id, type: :isAssigned}
%Diffo.Provider.Instance.Relationship{id: uni.id, alias: :uni, type: :isAssigned},
%Diffo.Provider.Instance.Relationship{id: avc.id, alias: :avc, type: :isAssigned}
]
})
pri = Nbn.mine_nbn_ethernet!(pri, %{})
}, actor: actor)
pri = Nbn.mine_nbn_ethernet!(pri, %{}, actor: actor)
pri |> Jason.encode!(pretty: true) |> IO.puts
```

Expand All @@ -246,6 +267,28 @@ Or from Elixir:
AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50")
```

## JSON API

The NBN domain exposes a JSON API via `Plug.Cowboy` on port 4000. Start the server in your application before evaluating these cells.

First check the catalog — all NBN specifications are initialised on startup:

```elixir
Req.get!("http://localhost:4000/catalog", decode_body: false).body |> IO.puts()
```

Now retrieve all NBN Ethernet instances:

```elixir
Req.get!("http://localhost:4000/nbnEthernet", decode_body: false).body |> IO.puts()
```

Or fetch the one we provisioned above by id:

```elixir
Req.get!("http://localhost:4000/nbnEthernet/#{pri.id}", decode_body: false).body |> IO.puts()
```

## What Next?

You've provisioned a complete NBN Ethernet access — NTD, UNI, AVC, CVC, NNI Group, and NNI — and seen how the `mine` actions propagate technology, speeds, CVLAN and port assignments up the resource hierarchy automatically.
Expand Down
23 changes: 23 additions & 0 deletions documentation/domains/nbn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>

SPDX-License-Identifier: MIT
-->

# The NBN Domain

## The Perentie Ecosystem

NBN Co operates as **Perentie** — Australia's largest monitor lizard, ancient and continent-wide. Perentie owns the territory. It does not compete with the animals moving through its country; it simply defines the ground they all walk on.

The RSPs are the spirit animals of the ecosystem, each finding their niche in Perentie's range:

| RSP | Spirit Animal | Inspiration |
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Wedge-tail Telecom | Wedge-tailed Eagle | Australia's apex aerial predator — dominant, territorial, commands every landscape it surveys |
| Quokka Connect | Quokka | Famously friendly, genuinely Australian, radiates good energy — operates in WA under bilateral agreement with Perentie |
| Ibis Telecom | White Ibis | Beloved in spite of its reputation, scrappy, surprisingly capable |
| Taipan Group | Taipan | Carries the TPG initials; fast, precise, not to be underestimated |
| Echidna Networks | Echidna | Prickly on the surface, uniquely capable beneath it |
| Dugong Digital | Dugong | Slow and steady, but still very much alive |
| Lyrebird | Lyrebird | Mimics everything, loops back on itself, endlessly clever |
Loading
Loading