Configuration
xApps are configured exclusively from a YAML file (cfg-mandatory) loaded
through the C SDK's ric_xapp_config_t — so Python, Go, and C xApps all
read the same xapp.yml through the same loader. The only CLI flag
an xApp takes is -c/--config <path> (defaulting to $AIRPULS_XAPP_CONFIG
or ./xapp.yml).
You do not have to write this from scratch
airpuls-sdk xapp new generates a valid xapp.yml with its
matching schema, xapp connect fills in the endpoint and TLS
identity, and xapp add sink writes the sink blocks. airpuls-sdk
xapp validate checks any hand-edit against the schema.
The schema
The schema splits into two top-level blocks: ric: says who to talk
to, xapp: says how the xApp behaves (plus any app-specific knobs it
registers).
ric:
endpoint: "tcp://127.0.0.1:36422" # required — RIC IPC endpoint
xapp:
deployment_name: "my-xapp" # required — operator-set identifier
# of one running deployment
log_level: "info" # verbose|info|warning|error|fatal|none
ping_interval_ms: 0 # 0 = disabled; >0 enables periodic pings
node_probe_interval_ms: 5000 # E2SM-EPI probe cadence; 0 = disabled
default_report_period_ms: 0 # period for a subscription that sets
# none; 0 = leave each as built
tick_interval_ms: 0 # Python BaseXApp.on_tick cadence; 0 = never
max_indications: 0 # Python BaseXApp auto-stop; 0 = run forever
reconnect:
enabled: true
initial_backoff_ms: 250
multiplier: 2.0
max_backoff_ms: 60000
jitter: 0.2
max_attempts: -1 # -1 = unlimited
# App-specific knobs, registered via add_param before load:
target_measurement: "DRB.UEThpDl"
report_period_ms: 1000
ric.endpoint and xapp.deployment_name are the two required keys;
load() fails loudly if either is missing. ric.endpoint accepts
tcp://, tls://, or unix://.
A tls:// endpoint additionally requires the ric.tls block — see
Mutual TLS below.
The JSON Schema beside the config
Every config ships with a JSON Schema (draft 2020-12) as a sibling file —
xapp.yml with xapp.schema.json, nrtric.yml with
nrtric.schema.json. Editors and deployment tooling locate it by that
colocation, and CI rejects a config that does not validate against it, an
xApp without one, and a schema missing a key the SDK registers.
The schemas carry x-airpuls annotations alongside the standard
keywords. JSON Schema ignores unknown x- keywords and neither loader
reads them, so they change nothing about validation or runtime — they
describe the configuration to tools. The one that matters when a
deployment renders configs from templates is deploy_token:
It names the placeholder a deployment renderer substitutes for that value, letting a form show what will be filled in without the config having to carry the key. Setting the value by hand is always valid; the annotation is a hint, not a requirement.
Required and validated keys
The C library validates connection parameters semantically before
spawning the I/O thread, so a bad value fails at connect time with a
clear error, before any network activity:
ric.endpointmust usetcp://,tls://, orunix://ric.tls.ca_file,ric.tls.cert_fileandric.tls.key_filemust be set together or not at all — a partial block is an error on any scheme- a
tls://endpoint requires all three - a non-
tls://endpoint with all three set logs a warning and connects in plaintext — scheme and material are meant to move together, so this flags a half-finished edit rather than a style xapp.deployment_namemust be non-empty, ≤255 chars, no control charsping_interval_ms,default_report_period_ms,reconnect.initial_backoff_ms,reconnect.max_backoff_msmust be ≥ 0reconnect.multiplier≥ 1.0;reconnect.jitter∈ [0.0, 1.0]reconnect.max_backoff_ms≥reconnect.initial_backoff_ms
The default reporting period
xapp.default_report_period_ms gives a reporting period, in
milliseconds, to a subscription whose builder sets none of its own. A
builder that sets its own period keeps it — the default never overrides
an explicit choice — and 0, the default, leaves every subscription
exactly as the xApp built it.
It reaches only the subscriptions that can carry a period:
| Service model | Applied to |
|---|---|
| KPM | every REPORT style — all of them are periodic |
| LLC | Style 2 only; Style 1 copies lower-layer information on a trigger |
| AIR | Style 2 only; Style 1 reports per SRS occasion |
| RC, CCC | not applied — their builders carry no reporting period |
The value is applied before the builder is validated, so a period the style rejects fails exactly as a hand-set one would, rather than producing a subscription that only fails when it materialises.
This is what lets an operator slow every periodic subscription an xApp makes without changing the xApp — useful against a busy node when the xApp exposes no period knob of its own.
Identity: deployment_name, xapp_type, xapp_version
xapp.deployment_name (YAML) is the operator's name for one running
deployment. The xApp's compile-time identity — xapp_type and
xapp_version — is set in code, not YAML, and both default to
"UNKNOWN" if unset. All of these surface as Prometheus labels
alongside xapp_instance (a persistent <deployment_name>+<8hex> from
the binding's state file) and xapp_id (RIC-allocated, security-bearing).
With BaseXApp (Python), set identity as class attributes
A BaseXApp subclass sets XAPP_TYPE / XAPP_VERSION as class
attributes and the base wires them into the config — you rarely call
set_xapp_type directly.
The instance identity: xapp.instance
xapp_instance is a restart-stable label for one running deployment,
resolved from configuration alone:
xapp.instance, when set, is used verbatim.- Otherwise the identity is
xapp.deployment_name.
That is the whole rule, and it is the same rule for every kind of
deployment. Nothing is read from or written to disk, so the value is
identical on every start without depending on $HOME, a writable state
directory, or a stable hostname — none of which hold for a process
running under a UID with no passwd entry, and the last of which is not
stable inside a container at all, where the hostname is the container ID.
Set xapp.instance when something outside the xApp assigns identities —
an SMO that names a deployment directory after the instance, or a k8s
operator. The value is carried through untouched and the RIC treats it as
opaque, so any scheme works:
Leave it unset for xApps nobody assigns an identity to; the deployment name then serves as the identity, which is what it already means.
One deployment name, one process
Whoever owns the identity also owns uniqueness. With xapp.instance
unset the SDK enforces it: a second process claiming the same
xapp.deployment_name on a host is refused with a CONFLICT log
line and fails to connect. The guard uses a lock file in
$XDG_RUNTIME_DIR (else $TMPDIR, else /tmp) and holds no state.
Setting xapp.instance bypasses it — an orchestrator that assigns
identities is assumed to know what it has already started.
The guard reaches as far as the filesystem it sits on, so an xApp
running in a container is guarded within that container, not against
a second container started from the same image. Container-level
duplication is the orchestrator's to prevent — a fixed
container_name in Compose, a replica count in Kubernetes.
Mutual TLS
When the RIC's xApp IPC listener runs on tls://, every xApp must too —
the two schemes cannot mix. Point ric.endpoint at the tls:// URI and
add the ric.tls block:
ric:
endpoint: "tls://airpuls-ric:36422"
tls:
ca_file: /etc/airpuls/tls/ca.crt # trust anchor for the RIC
cert_file: /etc/airpuls/tls/y1-termination.crt # this xApp's identity
key_file: /etc/airpuls/tls/y1-termination.key
xapp:
deployment_name: "y1-termination"
| Key | Required | Meaning |
|---|---|---|
ric.tls.ca_file |
yes | PEM CA the RIC's certificate must chain to. No system-store fallback: the RIC uses a private CA |
ric.tls.cert_file |
yes | PEM client certificate presented to the RIC |
ric.tls.key_file |
yes | PEM private key for cert_file |
ric.tls.server_name |
no | Expected identity in the RIC's certificate when it differs from the endpoint host. airpuls-sdk xapp connect records it from the listener's own certificate |
The certificate CN must equal deployment_name
The RIC treats the client certificate's Common Name as the xApp's
identity and refuses any connection whose Hello claims a different
deployment_name. A certificate issued for one xApp therefore cannot
be used by another. Ask your operator for a certificate whose CN is
exactly the deployment_name your xApp is deployed under.
All three files are read once at connect, so a replacement certificate is picked up on xApp restart. Operators: see the RIC's Security page for issuing and rotating them.
Extending the schema with app-specific knobs
Register custom parameters before load(); they then live under the
xapp: block and are read back with typed getters. Registration
declares the dotted path, type, whether it is required, and a default.
from airpuls_ric_sdk import RicXappConfig, ConfigType
cfg = RicXappConfig("/etc/airpuls/xapp.yml")
cfg.add_param("xapp.target_measurement", ConfigType.STRING, required=True)
cfg.add_param("xapp.report_period_ms", ConfigType.INT, default="1000")
cfg.load()
measurement = cfg.get_string("xapp.target_measurement")
period = cfg.get_int("xapp.report_period_ms")
cfg := ric.NewXappConfig("/etc/airpuls/xapp.yml")
defer cfg.Close()
cfg.AddParam("xapp.target_measurement", ric.ConfigString, true, "")
cfg.AddParam("xapp.report_period_ms", ric.ConfigInt, false, "1000")
cfg.Load()
measurement, _ := cfg.GetString("xapp.target_measurement")
period, _ := cfg.GetInt("xapp.report_period_ms")
ric_xapp_config_t *cfg = ric_xapp_config_new("/etc/airpuls/xapp.yml");
ric_xapp_config_add_param(cfg, "xapp.target_measurement",
RIC_CONFIG_STRING, true, NULL);
ric_xapp_config_add_param(cfg, "xapp.report_period_ms",
RIC_CONFIG_INT, false, "1000");
ric_xapp_config_load(cfg);
const char *measurement = ric_xapp_config_get_string(cfg, "xapp.target_measurement");
long period = ric_xapp_config_get_int(cfg, "xapp.report_period_ms");
With BaseXApp (Python)
Override register_params(self, config) — called once during
__init__, before config.load() — to register your knobs.
BaseXApp itself auto-registers xapp.max_indications (optional,
default 0 = run forever).
Logging
xapp.log_level is the single logging knob for the whole process — it is
applied to the C library, the Python logging root logger (via
BaseXApp.__init__), and Go's slog.Default (via ric.SetupLogging).
All three emit the same airpuls-format line:
Telemetry sinks
An optional top-level sinks: block configures where the xApp pushes its
own telemetry (Redis, InfluxDB, files). That is covered in full on the
Telemetry Sinks page.
A1 policy enforcement
An xApp that enforces O-RAN A1 policies adds an xapp.a1: block (the Redis
policy bus, namespace, and pacing). It is covered in full on the
A1 Policy Enforcement page.