Use OCUDU's own M-Plane tools to configure an O-RAN 7.2 radio unit over NETCONF, derive the DU's fronthaul configuration from it, and move the radio to a new frequency without restarting anything. The O-RU is simulated, so this runs on any node, with no radio and no reservation for an O-RU.
An O-RAN 7.2 radio unit (O-RU) has four planes. The control, user and synchronization planes (C/U/S-Plane) carry the radio data and timing between the DU and the O-RU. The management plane (M-Plane) configures the O-RU itself: which VLAN and MAC addresses its fronthaul uses, how the IQ samples are compressed, which carriers it radiates on, at what frequency, bandwidth and gain, and when they are active. The M-Plane is NETCONF (RFC 6241) over SSH, and its content is defined by the O-RAN WG4 YANG models (o-ran-uplane-conf, o-ran-processing-element, o-ran-module-cap, o-ran-delay-management, o-ran-supervision, …).
The OCUDU project ships the M-Plane outside the gNB, in its O-RAN applications:
ocudu_o1_adapter contains an O-RU M-Plane client (src/ru_config.py, and the command-line tool src/ru_controller.py). It writes a complete fronthaul configuration in the order a validating O-RU requires, activates the carriers, and derives the matching OCUDU ru_ofh configuration from what the O-RU reports.ocudu_netconf is a NETCONF server (netopeer2 + sysrepo) in a container. Started with --config ru it is a simulated O-RU: it loads the O-RAN WG4 YANG models and validates every edit against them, but nothing behind it drives a radio.This tutorial runs both on one node. The client provisions the simulated O-RU with the values of a COSMOS Benetel RAN650 cell, reads back the DU configuration it implies, and then moves the carrier from 3350.01 MHz to 3380.01 MHz using the M-Plane sequence for a frequency change. A small probe reads the O-RU's datastore directly after every step, so each check looks at what the O-RU stored rather than at the client's own report.
Moving an O-RU's carrier while the O-RU keeps running is the building block for dynamic spectrum use, for example moving a cell away from interference. The M-Plane side of that move is what this tutorial shows. The DU must move with it; in OCUDU 26.10 a carrier change in the DU still means restarting the DU (see References).
After completing this tutorial you will be able to:
gnb.yml ru_ofh section from an O-RU's M-Plane configuration;| Difficulty | Intermediate |
|---|---|
| Estimated time | 20 min (the first run downloads a 173 MB container image and the client's Python packages) |
| Domain / sandbox | any sandbox with servers, for example osc.cosmos-lab.org or sb1.cosmos-lab.org |
| Topic group | Cellular 4G/5G & O-RAN |
| Transmits? | No — the O-RU is simulated |
| Last verified | 2026-10-23 (three-mode parity on srv17.osc, baseline.ndz) |
Background knowledge
Account & access
docker.io, pulls a container image and installs Python packages).Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
one server, e.g. srv17 on osc |
runs the simulated O-RU and the M-Plane client | 1 | no radio, no fronthaul NIC needed |
Disk images
| Image | Load onto | Provides |
|---|---|---|
baseline.ndz |
the node | Ubuntu 24.04; the tutorial installs docker.io, git and python3-venv |
Software components (pinned in ocudu-mplane_params.yml)
| Component | Version | Source |
|---|---|---|
| simulated O-RU | ocudu_netconf image netconf_amd64:746ed5bc2c77… (2026-10-16) |
registry.gitlab.com/ocudu/ocudu_elements/ocudu_oran_apps/ocudu_netconf |
| M-Plane client | ocudu_o1_adapter commit dc07618c4011 (2026-10-09) |
gitlab.com/ocudu/ocudu_elements/ocudu_oran_apps/ocudu_o1_adapter |
| NETCONF library | ncclient 0.7.1 |
the client's requirements.txt |
Spectrum / RF / special
Not applicable: nothing radiates. The frequencies are the values the client writes into the simulated O-RU.
node (baseline.ndz)
┌────────────────────────────────────────────────────┐
│ OCUDU M-Plane client simulated O-RU │
│ ru_controller.py ──NETCONF/SSH──► ocudu_netconf │
│ (venv /opt/mplane-venv) 127.0.0.1:830 (docker, --config ru)
│ netopeer2 + sysrepo
│ ocudu-mplane_probe.py ──get-config──► O-RAN WG4 YANG datastore
└────────────────────────────────────────────────────┘
▲ ssh root@<node>
console.<sandbox>.cosmos-lab.org (runs the tutorial: manual / Ansible / Jupyter)
The NETCONF port is published on 127.0.0.1 only, so the simulated O-RU and its throwaway account are not reachable from other nodes.
ssh <username>@console.osc.cosmos-lab.org
baseline.ndz and turn the node on:omf tell -a offh -t srv17
omf load -i baseline.ndz -t srv17
omf tell -a on -t srv17
omf stat -t srv17
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-ocudu-mplane.zip
unzip -o cosmos-tutorials-ocudu-mplane.zip # extracts into ~/tutorials/
cd ~/tutorials
| File | What it is |
|---|---|
ocudu-mplane_params.yml |
the single source of every value (image, client commit, MACs, VLAN, carriers A and B) |
ocudu-mplane_manual.sh |
Mode 1, the commands below, scripted |
ocudu-mplane.yml |
Mode 2, Ansible |
ocudu-mplane_tutorial.ipynb |
Mode 3, Jupyter |
ocudu-mplane_probe.py |
reads the O-RU's datastore directly (YANG library, carriers) |
ocudu-mplane_lib.py |
the shared parser: every mode builds its result contract with it |
ocudu-mplane_parity.sh |
runs all three modes, checks that they agree, and runs the teardown on every exit |
ocudu-mplane_teardown.yml |
removes the simulated O-RU container |
Run these from the console. ocudu-mplane_manual.sh srv17 does exactly the same.
Step 1 — Install the tools on the node
ssh root@srv17 'apt-get update -qq && apt-get install -y -qq docker.io git python3-venv'
ssh root@srv17 'git clone -q https://gitlab.com/ocudu/ocudu_elements/ocudu_oran_apps/ocudu_o1_adapter.git /opt/ocudu_o1_adapter &&
cd /opt/ocudu_o1_adapter && git checkout -q dc07618c401180c348ec2f10d5e61239b8981f85 &&
python3 -m venv /opt/mplane-venv && /opt/mplane-venv/bin/pip install -q -r requirements.txt'
scp ocudu-mplane/ocudu-mplane_probe.py root@srv17:/root/
Step 2 — Start the simulated O-RU
ssh root@srv17 'docker run -d --name mock-oru -p 127.0.0.1:830:830 \
-e NETCONF_SSH_USERNAME=oru-admin -e NETCONF_SSH_PASSWORD=tutorial-only \
registry.gitlab.com/ocudu/ocudu_elements/ocudu_oran_apps/ocudu_netconf/netconf_amd64:746ed5bc2c77af6fe098946417aa69ef8bfd3e18 --config ru'
ssh root@srv17 'docker logs mock-oru 2>&1 | grep "Listening on"'
[INF]: LN: Listening on 0.0.0.0:830 for SSH connections.
Step 3 — Probe it: which O-RAN models does this O-RU implement?
ssh root@srv17 '/opt/mplane-venv/bin/python /root/ocudu-mplane_probe.py 127.0.0.1 830 oru-admin tutorial-only'
connected=1
n_capabilities=31
n_modules=100
has_o_ran_uplane_conf=1
has_o_ran_module_cap=1
has_o_ran_processing_element=1
has_o_ran_delay_management=1
has_o_ran_supervision=1
has_o_ran_sync=1
has_o_ran_hardware=1
A NETCONF 1.1 server lists its YANG modules in the ietf-yang-library (RFC 8525), not in its <hello>: 31 capabilities, 100 modules. No carriers are configured yet, so the probe prints none.
Step 4 — Provision the fronthaul
ssh root@srv17 'cd /opt/ocudu_o1_adapter && /opt/mplane-venv/bin/python src/ru_controller.py --host 127.0.0.1 --port 830 \
-u oru-admin -p tutorial-only --set_full_config --ru_mac_addr 8c:1f:64:d1:11:f0 --vlan 4 --du_mac_addr 00:11:22:33:44:66 \
--iq_bitwidth 9 --compression_type STATIC --rf_bandwidth_hz 100000000 \
--dl_arfcn 623334 --dl_freq 3350010000 --ul_arfcn 623334 --ul_freq 3350010000 --tx_gain 24 --carrier_state ACTIVE'
[INFO] Connected to 127.0.0.1:830 as oru-admin
[INFO] Editing IETF interfaces
[INFO] Editing ORAN processing elements
[INFO] Editing ORAN Uplane Tx endpoints elements
[INFO] Editing ORAN Uplane Rx endpoints elements
[INFO] Editing ORAN Uplane Tx array carriers
[INFO] Editing ORAN Uplane Rx array carriers
[INFO] Editing ORAN Uplane low level Tx links
[INFO] Editing ORAN Uplane low level Rx links
[INFO] Editing ORAN Uplane TDD pattern
[INFO] Editing ORAN Uplane carrier active
The order matters: later objects refer to earlier ones (a carrier refers to endpoints, a link refers to an endpoint, a carrier and a processing element). The O-RU validates every edit against its YANG models and answers with an error for anything that does not fit. NR-ARFCN 623334 is 3350.01 MHz (TS 38.104: 3000 MHz + 15 kHz × (N − 600000)).
Step 5 — Read back the DU configuration the O-RU implies
ssh root@srv17 'cd /opt/ocudu_o1_adapter && /opt/mplane-venv/bin/python src/ru_controller.py --host 127.0.0.1 --port 830 \
-u oru-admin -p tutorial-only --get_config'
# Auto-generated OFH configuration
ru_ofh:
cells:
- network_interface: uc-vlan4
ru_mac_addr: 8c:1f:64:d1:11:f0
du_mac_addr: 00:11:22:33:44:66
vlan_tag_cp: 4
vlan_tag_up: 4
dl_port_id: [0, 1, 2, 3] # shown as YAML lists in the real output
ul_port_id: [0, 1, 2, 3]
prach_port_id: [6, 7, 8, 9]
compr_method_dl: bfp
compr_bitwidth_dl: 9
is_prach_cp_enabled: true
cell_cfg:
nof_antennas_dl: 4
nof_antennas_ul: 4
channel_bandwidth_MHz: 100
dl_arfcn: 623334
These are sections of an OCUDU gnb.yml. Against a real O-RU the client also reads its delay profile (o-ran-delay-management) and derives the DU's fronthaul timing windows (T1a/Ta4); the simulated O-RU has no real delays.
Step 6 — Move the carrier to 3380.01 MHz
C="cd /opt/ocudu_o1_adapter && /opt/mplane-venv/bin/python src/ru_controller.py --host 127.0.0.1 --port 830 -u oru-admin -p tutorial-only"
ssh root@srv17 "$C --activate_carriers --carrier_state INACTIVE"
ssh root@srv17 "$C --set_carriers --rf_bandwidth_hz 100000000 --dl_arfcn 625334 --dl_freq 3380010000 --ul_arfcn 625334 --ul_freq 3380010000 --tx_gain 24"
ssh root@srv17 "$C --activate_carriers --carrier_state ACTIVE"
ssh root@srv17 '/opt/mplane-venv/bin/python /root/ocudu-mplane_probe.py 127.0.0.1 830 oru-admin tutorial-only | grep carrier'
tx_carrier=Tx-Array-Carrier-00|3380010000|100000000|ACTIVE
tx_carrier=Tx-Array-Carrier-01|3380010000|100000000|ACTIVE
tx_carrier=Tx-Array-Carrier-02|3380010000|100000000|ACTIVE
tx_carrier=Tx-Array-Carrier-03|3380010000|100000000|ACTIVE
rx_carrier=Rx-Array-Carrier-00|3380010000|100000000|ACTIVE
...
The O-RAN M-Plane changes a carrier's frequency only while it is INACTIVE, so the move is: deactivate, change, activate. The O-RU process keeps running throughout. The three steps take about 2 s here, most of it the start-up of three separate client processes.
cd ~/tutorials
export OMF_NODES="srv17"
ansible-playbook ocudu-mplane/ocudu-mplane.yml
The playbook runs the same steps with the same parameters and ends by printing the result contract.
Open ocudu-mplane/ocudu-mplane_tutorial.ipynb in the console's Jupyter (kernel Python 3 (cosmos-jupyter)), set OMF_NODES in the first cell, and run all cells. The notebook adds explanations between the steps.
cd ~/tutorials
./ocudu-mplane/ocudu-mplane_parity.sh srv17
### MODE 1/3: ansible
ok
### MODE 2/3: manual
ok
### MODE 3/3: jupyter (papermill)
ok
=== ocudu-mplane parity ===
mode n_modules provision_steps n_tx_carriers derived_dl_arfcn derived_vlan provisioned retuned retune_ms
------------------------------------------------------------------------------------------------------------
ansible 100 9 4 623334 4 True True 2107
manual 100 9 4 623334 4 True True 2103
jupyter 100 9 4 623334 4 True True 2510
PARITY: PASS (same experiment in all modes; all checks green)
### releasing the node (teardown)
node released
Each mode starts its own fresh simulated O-RU, so no mode can read another's configuration.
The run is correct when the result contract (mplane_contract_<mode>.json) shows:
| Field | Expected | Meaning |
|---|---|---|
connected, has_all_modules |
true |
NETCONF session up; the seven O-RAN WG4 models are in the YANG library |
provision_complete |
true |
all nine provisioning edits sent, no error from the O-RU |
provisioned |
true |
the O-RU stores 4 TX + 4 RX carriers at 3350010000 Hz, all ACTIVE |
derived_ok |
true |
the derived DU config carries dl_arfcn 623334 and VLAN 4 |
retuned |
true |
the O-RU stores all carriers at 3380010000 Hz, ACTIVE, after the move |
retune_ms |
≤ 30000 | time for the three-step move (≈2 s measured) |
cd ~/tutorials && export OMF_NODES="srv17"
ansible-playbook ocudu-mplane/ocudu-mplane_teardown.yml # removes the mock-oru container
omf tell -a offh -t srv17
| Symptom | Likely cause | Fix |
|---|---|---|
docker run fails with pull access denied or a timeout |
the node has no route to registry.gitlab.com |
check the node's internet access: ssh root@<node> 'curl -sI https://registry.gitlab.com' |
the probe prints connected=0 … Authentication failed |
the container was started without NETCONF_SSH_USERNAME/NETCONF_SSH_PASSWORD, or with other values than the params |
remove and restart the container with the values from ocudu-mplane_params.yml |
the probe prints connected=0 … Connection refused right after docker run |
the NETCONF server is still starting | wait for Listening on 0.0.0.0:830 in docker logs mock-oru |
has_o_ran_* = 0 from your own NETCONF client |
it checked the <hello> capabilities |
a NETCONF 1.1 server lists modules in ietf-yang-library; read that (the probe does) |
an edit is refused with an rpc-error |
a value does not satisfy the YANG model (e.g. a MAC or VLAN out of range) | read the error text; the simulated O-RU validates exactly like a real one |
pip install fails building lxml |
the node has no internet access, or an old pip | check internet access; /opt/mplane-venv/bin/pip install -U pip and retry |
mplane_arfcn_a / mplane_arfcn_b in the params (n78 is NR-ARFCN 620000–653333).--enable-callhome 172.17.0.1:4334 (docker's default bridge gateway, i.e. the node itself) and run the client on the node with --callhome to see an O-RU that dials its manager instead of listening.ru_controller.py --supervise keeps an O-RU's supervision watchdog alive. The simulated O-RU has no application behind the watchdog, so it answers every reset with an error; a real O-RU is needed for that path.ocudu_o1_adapter docs/mplane-client.md and docs/mplane-sim-testing.md — the sim validates schema, protocol and configuration management, not radio behaviour.ocudu_netconf. In the O1 path, only SSB power, the RRM policy ratio and PM jobs change at runtime in the DU; any other change, including the carrier frequency, restarts the DU (src/config_manager.py, _RUNTIME_UPDATABLE_PARAMS).Author(s): COSMOS team · Last verified: 2026-10-23 (three-mode parity on srv17.osc, baseline.ndz) · Tested image/release: baseline.ndz + ocudu_netconf 746ed5bc2c77 + ocudu_o1_adapter dc07618c4011 · Bundle: cosmos-tutorials-ocudu-mplane.zip · Tags: ocudu, oran, m-plane, netconf, yang, o-ru, 7.2