Skip to content

The airpuls-sdk CLI

airpuls-sdk is the developer tool for xApp projects. It generates a project that already validates, runs it against a RIC, builds its image, and deploys the entry a RIC host consumes — so the only thing you write by hand is the xApp logic.

pip install airpuls-sdk-cli

Pure Python, published as a py3-none-any wheel: it installs wherever Python 3.9+ runs. The command it installs is airpuls-sdk.

Two distributions, two jobs

Do not confuse the tool with the runtime — they are separate packages with separate platform support:

Distribution What it is Platforms
airpuls-sdk-cli this tool — scaffolding, lifecycle, deployment anywhere Python runs
airpuls-ric-sdk the xApp runtime: the Python binding over libric_client Linux x86-64 and aarch64 (binary wheels)

Most commands need only the CLI. The two that execute project code — xapp run, and xapp test for suites that import the SDK — need the runtime in the environment running the project. xapp venv provisions exactly that.

pip install airpuls-sdk-cli[runtime]    # Linux x86-64 / aarch64

On macOS and Windows the runtime has no wheel. That is what the generated Dev Container is for — xapp new writes a .devcontainer/ reproducing the environment the runtime needs, so you create the project natively and run it inside the container. airpuls-sdk doctor reports which half is present, and xapp run explains a missing runtime instead of failing on an import error.

Quick start

From nothing to an xApp running against a RIC:

airpuls-sdk xapp new my-xapp     # interactive wizard (--defaults for CI);
                                 # provisions .venv (--no-venv skips)
cd my-xapp
airpuls-sdk xapp validate        # xapp.yml against xapp.schema.json
airpuls-sdk xapp run             # foreground run against the configured RIC

xapp new asks for the name, the deployment identity, the RIC endpoint, the service model the skeleton subscribes to, and the image reference the launcher will run; every answer has a default, and --defaults (or the absence of a terminal) takes them all. The generated project is validated against its own schema before the command reports success, so a project that generates at all is a project that validates.

Prompts never pre-fill

Every question shows its default in brackets — Endpoint [tcp://127.0.0.1:36422]: — and an empty answer takes it. Nothing is typed into the input line for you to delete.

What a generated project looks like

my-xapp/
    xapp.yml              # runtime configuration — see Configuration
    xapp.schema.json      # the schema xapp.yml is validated against
    my-xapp.xapp          # executable docker-run launcher (the deploy entry)
    Dockerfile            # builds the image the launcher references
    requirements.txt      # production dependencies (the runtime)
    tests/requirements.txt# test-only dependencies (pytest + the CLI)
    README.md             # generated project overview
    src/main.py           # your xApp — a BaseXApp subclass that subscribes
    tests/                # pytest suite, runs without the runtime
    .devcontainer/        # macOS / Windows development environment
    .airpuls-xapp.yml     # scaffold record: the template and the answers used

.airpuls/logs/ appears the first time a command captures output.

src/main.py is a working xApp for the selected service model — it connects, registers the plugin, resolves the RAN function by OID, subscribes on node availability, and logs each indication. Replace its body; the surrounding lifecycle is the one Building xApps describes.

The scaffold generates Python + KPM today

--language accepts python and --sm accepts kpm; the other languages and service models are reachable from a generated project by hand (and from an airpuls-ric checkout — see Packaging), but the wizard does not emit them yet. Everything after xapp new — validate, run, package, deploy — is language-agnostic.

The image base is chosen at generation time with --base:

--base Result
sdk builds on airpuls/airpuls-xapp-sdk (tag latest, or pin one with --base-tag), which carries the C SDK, the SM archives, and the shared service dependencies
slim builds on a plain Python image and installs the published runtime from requirements.txt — the project then needs nothing from an airpuls-ric checkout

The command surface

Project lifecycle

Command What it does
xapp new NAME generate a project, git init it, and provision .venv (--no-venv skips)
xapp venv create or repair .venv and install requirements.txt + tests/requirements.txt; safe to repeat
xapp validate validate xapp.yml against the sibling xapp.schema.json
xapp info one-screen project summary — identity, endpoint, language, schema metadata
xapp build build the project's language implementation, output captured
xapp test run the project's tests/ suite, output captured
xapp run run the xApp in the foreground against the RIC in xapp.yml
xapp package build the <name>-<language>:latest image on the SDK base image
xapp add sink … add a telemetry destination to xapp.yml

xapp run takes three overrides that never touch xapp.yml: --config runs a different file, --certs-dir remaps the configured ric.tls paths into a directory by file name, and --server-name sets the identity to expect in the RIC listener's certificate for that run only.

Deployment

Command What it does
config ric add NAME --host … --path … register a RIC deployment target
config ric list / use NAME / remove NAME manage the registry and its default
xapp connect point ric.endpoint at a registered RIC
xapp credentials register the xApp's identity with the RIC and fetch its mTLS material
xapp deploy build + publish the image, upload the entry to the host
xapp undeploy remove the entry from the host
doctor environment diagnostics, one row per check

Run inside an xApp project, doctor adds project-scoped rows: whether the project's .venv is provisioned, and whether requirements.txt and tests/requirements.txt are satisfied by what is installed in it — a freshly cloned project warns on all three until airpuls-sdk xapp venv provisions the environment.

Several RICs can be registered; every operation picks one with --ric, the registry default, or an interactive list. --host, --user, --port, --path, and --identity-file override the stored values per command.

Connecting a project to a RIC

Register the RIC once, then point the project at it:

airpuls-sdk config ric add lab \
    --host 192.168.3.1 --user airpuls \
    --path /home/airpuls/nrtric-ctrl --default

airpuls-sdk xapp connect          # writes ric.endpoint into xapp.yml
airpuls-sdk xapp credentials      # identity + CA/cert/key into ./certs
airpuls-sdk xapp run              # xapp run picks certs/ up automatically

--path is the nrtric-ctrl checkout on the host; deployed entries land in its xapps/ folder. config ric add offers a one-time ssh-copy-id on a terminal, which makes every later connection passwordless.

Against a tls:// listener, xapp connect reads the listener's own server certificate and records its Common Name as ric.tls.server_name, so verification holds even when the certificate's subject alternative names do not cover the address the xApp dials. The read is advisory — if it fails, the endpoint is still written, with a warning.

Telemetry sinks

xapp add sink writes a sink block into xapp.yml — the same block Telemetry Sinks documents:

airpuls-sdk xapp add sink                              # asks what to add
airpuls-sdk xapp add sink --file ./data/events.jsonl   # structured
airpuls-sdk xapp add sink --redis redis://host:6379/0  # both channels
airpuls-sdk xapp add sink --influxdb http://host:8086  # timeseries

A destination serving both channels is added to both; --channel timeseries|structured narrows it. --disabled adds a destination without switching it on. Only the keys the destination determines are written, the manifest is validated before it is saved, and comments in the file survive the edit — so a sink never leaves xapp.yml violating its own contract. An already-configured sink is reported rather than replaced.

A relative --file path is stored relative to the project root (./data/events.jsonl becomes data/events.jsonl); the sink itself creates the directory on start. Keep it under the project root — deploy remaps a relative path onto the container's /xapp/data.

A secret typed as an argument would be stored

--token-env NAME (InfluxDB, default INFLUX_TOKEN) and --password-env NAME (Redis) record the name of the environment variable holding the secret, never the secret. A value that is not a variable name is refused rather than written: xapp.yml is committed with the project and uploaded to the deployment host. The interactive prompts ask for the variable name too.

The InfluxDB organization and bucket default to what the deployment provisions — a fixed xapp_metrics bucket for xApps beside the RIC's own — so a URL alone is a complete destination.

Deploying

Deployment follows the nrtric-ctrl pattern. Every generated project carries an executable <name>.xapp docker-run launcher; xapp deploy builds the image that launcher references, pushes it to the registry the reference names, then uploads the entry the stack consumes — launcher, xapp.yml, xapp.schema.json — into <nrtric-ctrl>/xapps/<name>/ on the host.

airpuls-sdk xapp deploy --ric lab

The project source is never uploaded: the image is the runtime artifact and the host pulls it. The image is tagged with the launcher's own reference, so what is pushed is what the host resolves — which means that reference must name a registry (registry.example.de/xapps/my-xapp:latest, given by --image on xapp new). The scaffold default (<name>-<language>:latest) names none, so it would push to Docker Hub; xapp deploy refuses it and says so.

Two alternatives to the registry:

Flag Effect
--image-copy build, then stream the image into the host's daemon over SSH (docker save | docker load) — for a host that cannot reach the registry. The image must match the host's architecture
--no-image upload the entry alone, building and publishing nothing — a configuration-only redeploy

The uploaded xapp.yml is a copy, adapted to the container

The project's own file is never edited. A relative file-sink path moves onto /xapp/data — the directory the launcher mounts from the deployed entry, and the only one a container running as the invoking user can write. Keep sink paths under the project's data/ directory and one setting serves both sides.

What the deploy cannot resolve it names rather than rewrites: an absolute path has to be mounted, and a 127.0.0.1 destination means the xApp's own container once it is on the host.

TLS through the whole flow

The RIC's own listener configuration is the source of truth for the transport, and xapp deploy reads it before uploading:

  • against a tls:// listener it mints the client certificate (CN = deployment name, the RIC's identity binding), places CA, certificate, and key beside the deployed launcher, and — when the project still dials tcp:// — adapts the uploaded copy, so a scheme mismatch cannot be deployed;
  • against a plaintext listener a tls:// project is refused: the CLI never downgrades a secured configuration.

See Configuration → Mutual TLS for the ric.tls block itself.

Output and exit codes

Subcommand output is never passed through to the terminal. The CLI captures it, reports the outcome with its cause and an excerpt on failure, and writes the full transcript under the project's .airpuls/logs/. Exit codes are typed:

Code Meaning
1 generic failure
2 usage error
3 validation failure
4 subprocess failure
5 toolchain / environment problem

JSON output for editors and scripts

--json, placed before the command (or AIRPULS_SDK_JSON=1), makes every command write exactly one JSON document on stdout instead of rendered text:

airpuls-sdk --json xapp info
airpuls-sdk --json doctor            # one record per environment check
airpuls-sdk --json config ric list   # registered targets, for a picker
{"schema": 1, "ok": true, "command": "xapp info",
 "data": {"name": "my-xapp", "endpoint": "tls://ric:36422"},
 "warnings": []}

schema, ok, command, data, and warnings are common to every command; data fields are per command. A failure adds an error block — kind (usage, validation, subprocess, toolchain, interrupted, internal), message, cause, hint, detail, and the transcript path in log. The exit status is the same in both modes, so a caller branches on the status and reads the document only when it is non-zero.

--json --help answers with the command surface as data — the subcommands a group holds, and the arguments and options a command accepts with their defaults — so an integration builds its own affordances without scraping help text.

The mode also makes every command non-interactive: nothing prompts, so a missing answer fails with a usage error naming the option to pass (xapp new needs its options or --defaults, xapp undeploy needs --yes) rather than blocking on a question nothing can answer. Warnings become warnings entries, and the streamed output of xapp run moves to stderr so stdout stays a single document.

The IDE plugins drive this surface

The VS Code and JetBrains plugins are thin clients over --json. Anything they show, a script can read the same way.

Platform support

Linux macOS Windows
install, xapp new, validate, info, build, add sink, config, doctor yes yes yes
xapp connect, deploy, undeploy, credentials yes yes SSH keys only
xapp run, xapp test yes Dev Container Dev Container
xapp package needs docker needs docker needs docker

xapp deploy builds the launcher's image, so it needs docker too — except with --no-image.

The deployment commands drive the OpenSSH tools. Windows ships an OpenSSH client, so key-based authentication works, but the interactive password fallback, the one-time ssh-copy-id offer, and the restrictive mode applied to a fetched client key all rely on POSIX mechanisms and do not. A Windows user therefore needs a key the deployment host already trusts.

Next steps