The Fronthaul Agent
Everything above an O-RU's IP address is reconciled by a controller or a sidecar.
Below it is the node: the fronthaul port, the segment the O-RU sits on, the address
the O-RU has to ask for, whether it answers at all. The fronthaul agent is Racora
there. It runs on every fronthaul-ready node, outlives every DU pod, gives each
O-RU a cell names its address, and tells Kubernetes one thing per O-RU: whether it
can be reached right now.
One Agent per Node, One Device per O-RU
The agent is the DaemonSet fronthaul-agent in racora-system, shipped by the
racora-node chart next to the USRP and NVIDIA device plugins, and scheduled by the
node contract's label racora.io/fronthaul-ready: "true". It watches the NRCells
and takes responsibility for every cell with radioBackend.ruType: ofh that names
an M-plane address (radioBackend.ofh.mplane.netconfHost) and is bound to its node
(duAssignment.nodeName) or to no node at all. A cell bound to another node is that
node's agent's; a cell that only sets callHome has no address to probe and gets no
device.
Every ten seconds the agent opens a TCP connection to each O-RU's NETCONF port and
closes it again. The connection is bound to the node's fronthaul interface (the
cell's networkInterface, else fronthaul_nic from the provisioner's inventory), so
only an O-RU on that port's segment can answer: an address that happens to be
routable over the management network does not count. The result is one of:
| The agent logs | Healthy | What it means |
|---|---|---|
ready | yes | the connection opened: the O-RU is up and its NETCONF server listens |
netconf-closed | no | the O-RU answers but refuses the port: it is booting and NETCONF is not up yet |
unreachable | no | nobody answers: powered off, resetting, without an address, or not cabled to this port |
off-segment | no | the address has no route over the fronthaul port: the cell names a host on another network |
no-fronthaul-interface | no | the node has no inventory, or the cell names an interface the node does not have |
The agent is a device plugin:
it registers the resource racora.io/oru with the kubelet and reports one
device per O-RU, named after the cell, Healthy or Unhealthy. On the Node that shows
as Capacity (how many O-RUs the node's cells name) and Allocatable (how many
answer):
kubectl describe node <node> | grep -A1 'racora.io/oru'
kubectl -n racora-system logs ds/fronthaul-agent # one "radio health" line per change, with the reason
The device-plugin API carries Healthy or Unhealthy and nothing more, so the reason is in the agent's log.
The O-RU's Address
An O-RU attaches the way the O-RAN M-plane specification says: it asks for an address with DHCP, naming itself with its vendor class, and reads its M-plane controller's address out of the answer's vendor-specific options. It asks untagged first and then on one VLAN tag after another, a few seconds each, until something answers; the tag it is answered on becomes its M-plane VLAN, and most O-RUs forget it on every reset and scan again. A DHCP server that listens untagged hears only the first attempt and, when it misses that one, nothing for hours. An O-RU that has been scanning for a long time may not even keep a mid-pass answer: one left for days took each tagged lease, dropped it three seconds later and moved on, and kept only the one it was given at the end of its range.
The agent is the answerer, on the fronthaul port, for exactly the O-RUs the node's
cells name. The reservation is the cell: a request from the cell's ruMacAddr (on
the cell or in its radio profile) gets the cell's netconfHost, the port's subnet
mask, a lease (racora-node.nodePlugins.fronthaul.dhcp.lease, an hour by default)
and the O-RAN vendor options with the node's port address as the M-plane
controller (option 43, sub-option 129; sub-option 130 adds an FQDN when
dhcp.controllerFqdn is set), on the tag the request came in on. Answered on a
VLAN, the O-RU runs its M-plane there, so the agent gives the node a VLAN
sub-interface for it (fhmp<id>) and a host route to the O-RU through it, and
points its own probe at that interface; answered untagged, the O-RU is on the port
itself. The cell's own vlanTagCp/vlanTagUp are the fronthaul stream's VLAN and
are unaffected. There is no lease table to lose: the reservations are the cells,
so a restarted agent acknowledges the O-RU's renewals as before, and it reads the
VLAN sub-interfaces and routes it left on the node, so an O-RU attached on a VLAN
is still reached there after the restart; sub-interfaces no cell uses any more are
removed.
A request from an O-RU no cell names is not answered. It is logged with its MAC,
vendor class and VLAN, and posted once per ten minutes as an Event UnknownORU on
the node: that is how a radio nobody has declared yet becomes visible.
kubectl -n racora-system logs ds/fronthaul-agent | grep -E 'DHCP|attached|no cell names'
kubectl get events -A --field-selector reason=UnknownORU # also under kubectl describe node <node>
The cell's address has to be an IPv4 address in the port's subnet, or the agent says once in its log that it cannot serve it. An O-RU with a static address is simply never asked for; the agent only probes it. Another DHCP server on the same segment would answer alongside the agent, so there must not be one.
The Link and the Clock
Below the O-RU's address is the port itself, and below the port the clock. The agent holds the first and watches the second.
The link. On every link event (carrier, flags, a re-created device) and once a
minute besides, the agent re-asserts what an O-RU needs from the fronthaul port and
what it has the privileges for: hardware VLAN receive filtering off (with it on,
the NIC drops every tagged frame, so neither the O-RU's tagged uplink nor its DHCP
scan reaches the host), the MTU (racora-node.nodePlugins.fronthaul.link.mtu, 9000)
and the transmit queue length (link.txQueueLen, 20000). What it can only read it
reports: idle states deeper than 10 µs still enabled on the isolated cores, and the
port's IRQs sitting on those cores. All of this is set once by the provisioner's
fronthaul stage and persisted across boots (Node Provisioner);
the agent is for a NIC reset, a driver reload, a host provisioned before a stage
existed, and for seeing it when it drifts.
The clock. A radio cannot lock to a clock that is not there or not right, and
the tools that read the clock (pmc, phc_ctl, the PTP hardware clock's device) live
on the host. So the host writes and the agent reads: the provisioner's
racora-ptp-status timer writes the clock's state to /run/racora/ptp-status every
ten seconds (whether ptp4l and phc2sys run, the PTP port state, the NIC clock's
frequency adjustment and its offset from the system clock, the UTC offset), and the
agent folds it into every O-RU's health: ptp4l not running, the port not MASTER
(grandmaster mode) or not SLAVE (ordinary clock), or a NIC clock that is not on
TAI (not utc_offset seconds ahead of the system clock: the O-RU and the DU would
number frames 37 s apart) make every device on the node Unhealthy with the reason
ptp-not-ready, so no DU turns up toward a radio that cannot lock. No status file,
or one older than a minute, is no information: the health is then the probe's alone.
On a cluster you run, install the same timer or leave the clock out of the verdict.
Metrics. The agent serves what it sees on the node's metricsPort (9478):
/metrics in the Prometheus text format and /healthz. Per O-RU: healthy, the
reason, the VLAN it was attached on, the DHCP replies sent; for the node: the link
settings, the IRQ and idle-state counts, the clock's state, and the count of
requests from O-RUs no cell names. The monitoring stack's collector scrapes every
agent into ClickHouse (otel_metrics_gauge, otel_metrics_sum), and Grafana's
Open Fronthaul dashboard shows them per cell
(Read Logs, Traces, and Metrics).
curl -s http://<node>:9478/metrics | grep racora_fronthaul_
How the DU Uses It
The controller gives every ofh DU with an M-plane address
resources.limits: {racora.io/oru: 1}. The scheduler then places the pod only on a
node with a healthy device, and until there is one the pod stays Pending with
Insufficient racora.io/oru in its events. A DU therefore never starts toward an
O-RU that nobody reaches, and it starts on the node whose port reaches it: with one
fronthaul-ready node the cell need not name the node at all. A cell that only sets
callHome requests nothing, since there is no device to request, and schedules by
the label alone.
The scheduler counts devices; it does not pick one by name. With several O-RUs on
one node a cell must name its node, and one ofh DU runs per node in any case (the
DU holds the node's host ports). The device a DU was given is in its environment
as RACORA_ORU_CELL.
How the Controller Uses It
While the DU runs, the kubelet writes the device's health into the pod's own status
(containerStatuses[].allocatedResourcesStatus, Kubernetes 1.36). The controller
reads that pod every ten seconds anyway, for RadioReady and DUReady, so an O-RU
the agent stops reaching becomes RadioReady=False, reason Unreachable on
the cell, with the DU still DUReady=True. The same reason covers a pod the
scheduler cannot place for want of a healthy device.
A DU streaming toward an O-RU that no longer answers does not recover by itself:
the O-RU has reset, lost power or its cable, and when it comes back it is
unprovisioned. So after a grace, racora-controller.ofhUnreachableGraceSeconds
(90 s by default; long enough for a blip, shorter than an O-RU's reboot), the
controller deletes the pod and posts the Warning Event RadioUnreachable on the
cell. The replacement pod requests the device again, waits Pending until the agent
reports the O-RU healthy, and then its mplane sidecar provisions the O-RU from
scratch before the new DU starts. The whole sequence needs nobody: it is the same
path a DU restart takes (DURestarted), entered from the other side.
kubectl -n racora-system get nrcell <cell> -o jsonpath='{range .status.conditions[*]}{.type}={.status}/{.reason}{"\n"}{end}'
kubectl -n racora-system describe nrcell <cell> # the RadioUnreachable and DURestarted Events
What It Does Not Do
The agent attaches, looks and reports. It opens no NETCONF session and holds no credentials: the M-plane is the sidecar's, and the agent's probe is a plain TCP connect the O-RU's NETCONF server sees as a connection without a login. It does not assign the port's own address, and it does not touch the PTP services the provisioner set up. An O-RU that is unreachable and silent needs a hand on it (power, cable, console), and the agent's job is to make that visible on the cell within seconds instead of leaving a DU pod to start toward nothing.
Operating It
| Value | Default | Meaning |
|---|---|---|
racora-node.nodePlugins.fronthaul.enabled | true | run the agent on fronthaul-ready nodes |
racora-node.nodePlugins.fronthaul.probeInterval | 10s | how often every O-RU is probed |
racora-node.nodePlugins.fronthaul.probeTimeout | 2s | one probe's connect timeout |
racora-node.nodePlugins.fronthaul.nodeSelector | racora.io/fronthaul-ready: "true" | where the agent runs |
racora-node.nodePlugins.fronthaul.dhcp.enabled | true | serve the O-RUs' addresses on the fronthaul port |
racora-node.nodePlugins.fronthaul.dhcp.lease | 1h | the lease time |
racora-node.nodePlugins.fronthaul.dhcp.controllerFqdn | "" | an M-plane controller FQDN to announce as well |
racora-node.nodePlugins.fronthaul.link.enforce | true | hold the port's VLAN filter, MTU and queue length |
racora-node.nodePlugins.fronthaul.link.mtu | 9000 | the MTU to hold (0 leaves it) |
racora-node.nodePlugins.fronthaul.link.txQueueLen | 20000 | the transmit queue length to hold (0 leaves it) |
racora-node.nodePlugins.fronthaul.metricsPort | 9478 | /metrics and /healthz on the node; empty disables |
racora-controller.oruResource | racora.io/oru | the resource ofh DUs request; empty, they request nothing |
racora-controller.ofhUnreachableGraceSeconds | 90 | how long an O-RU may be unhealthy under a running DU before the pod is replaced |
Turning the agent off means both nodePlugins.fronthaul.enabled=false and
racora-controller.oruResource=""; with the agent gone and the request kept, no
ofh DU schedules. The chart values are on Chart Values
and the HelmChartConfig that sets them on Change Chart Values.
The agent runs on the host network (the fronthaul interface is the node's) with the
NET_RAW capability for the raw socket and the interface bind and NET_ADMIN for
the VLAN sub-interface, the route and the link settings, nothing else; mounts the
kubelet's device-plugin directory, /etc/racora and /run/racora read-only; reads
NRCells and ConfigMaps (the radio profiles) and may post Events
(Ports and Privileges).