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.
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.
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.
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 dialstcp://— 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
- Configuration — the
xapp.ymlthe CLI generates - Building xApps — the lifecycle inside
src/main.py - Service Model Guides — what to subscribe to, per SM
- Telemetry Sinks — the sink blocks
xapp add sinkwrites - Packaging — the image
xapp packagebuilds