Skip to content

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:

"ca_file": {
  "type": "string",
  "x-airpuls": { "deploy_token": "DEFAULT_CA_FILE_PATH" }
}

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.endpoint must use tcp://, tls://, or unix://
  • ric.tls.ca_file, ric.tls.cert_file and ric.tls.key_file must 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_name must be non-empty, ≤255 chars, no control chars
  • ping_interval_ms, default_report_period_ms, reconnect.initial_backoff_ms, reconnect.max_backoff_ms must be ≥ 0
  • reconnect.multiplier ≥ 1.0; reconnect.jitter ∈ [0.0, 1.0]
  • reconnect.max_backoff_msreconnect.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).

from airpuls_ric_sdk import RicXappConfig, ConfigType

cfg = RicXappConfig("/etc/airpuls/xapp.yml")
cfg.set_xapp_type("my-xapp")        # compile-time identity
cfg.set_xapp_version("1.0.0")
cfg.load()
cfg := ric.NewXappConfig("/etc/airpuls/xapp.yml")
defer cfg.Close()
cfg.SetXappType("my-xapp")
cfg.SetXappVersion("1.0.0")
if err := cfg.Load(); err != nil {
    log.Fatal(err)
}
ric_xapp_config_t *cfg = ric_xapp_config_new("/etc/airpuls/xapp.yml");
ric_xapp_config_set_xapp_type(cfg, "my-xapp");
ric_xapp_config_set_xapp_version(cfg, "1.0.0");
ric_xapp_config_load(cfg);

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:

  1. xapp.instance, when set, is used verbatim.
  2. 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:

xapp:
  deployment_name: "my-xapp"
  instance: "my-xapp-7f3c1a"     # assigned by the orchestrator

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:

YYYY-MM-DD HH:MM:SS.mmm [LEVEL  ] [module] message (file:line)
from airpuls_ric_sdk.xapp import setup_logging
setup_logging()   # BaseXApp also applies xapp.log_level on init
ric.SetupLogging(nil, "my-xapp")   // INFO default before cfg.Load()
// ... cfg.Load() ...
ric.SetupLogging(cfg, "my-xapp")   // re-apply with cfg's xapp.log_level
/* The client applies xapp.log_level from the config at connect time;
 * override programmatically with the airpuls_common log API. */
airpuls_log_set_level(AIRPULS_LOG_INFO);

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.