Metadata-Version: 2.4
Name: airpuls-sdk-cli
Version: 0.3.3.dev64
Summary: Command-line tool for airpuls Near-RT RIC xApps: project scaffolding, lifecycle, and deployment.
Author: airpuls GmbH
License-Expression: Apache-2.0
Project-URL: Homepage, https://airpuls.de
Keywords: oran,ric,near-rt-ric,xapp,cli,scaffolding
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Telecommunications Industry
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: System :: Networking
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13.7
Requires-Dist: questionary>=2.0
Requires-Dist: jinja2>=3.1
Requires-Dist: jsonschema>=4.21
Requires-Dist: packaging>=24.0
Requires-Dist: ruamel.yaml>=0.18
Provides-Extra: runtime
Requires-Dist: airpuls-ric-sdk>=0.3.3.dev64; extra == "runtime"
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Dynamic: license-file

# `airpuls-sdk` — command-line tool for airpuls Near-RT RIC xApps

Scaffolds, validates, runs, packages, and deploys xApp projects for the
airpuls Near-RT RIC.  Distributed as `airpuls-sdk-cli`; the command it
installs is `airpuls-sdk`.

```bash
pip install airpuls-sdk-cli
```

Pure Python, published as a `py3-none-any` wheel: the tool installs
wherever Python 3.9+ runs, so a project can be created from any
developer machine.  Linux and macOS are the supported platforms for the
full workflow — see [Platform support](#platform-support).

## Two distributions, two jobs

| Distribution | What it is | Platforms |
|---|---|---|
| `airpuls-sdk-cli` | this tool — scaffolding, lifecycle, deployment | Linux, macOS (installs anywhere Python runs) |
| `airpuls-ric-sdk` | the xApp runtime: Python bindings for the C client library | Linux x86_64 and aarch64 (binary wheels) |

Most commands need only the CLI.  Running an xApp locally —
`xapp run`, and `xapp test` for suites that import the SDK — needs the
runtime in the environment that runs the project.  `xapp new`
provisions exactly that by default: it creates the project's `.venv`
and installs `requirements.txt` (production dependencies — the
runtime on Linux) and `tests/requirements.txt` (pytest and the CLI
the generated tests import) into it — `--no-venv` skips the step,
and `xapp venv` provisions an existing checkout at any time.  The
commands that execute project code resolve their interpreter in that
order — an activated virtual environment first, then the project's
`.venv`, then the interpreter running the CLI — so a provisioned
project runs correctly whether the CLI is invoked from a shell or by
an editor.  Installing the runtime beside the CLI works too:

```bash
pip install airpuls-sdk-cli[runtime]   # Linux x86_64 / aarch64
```

On macOS and Windows the runtime has no wheel, which is what the
generated Dev Container is for: `xapp new` writes a `.devcontainer/`
whose Dockerfile pins an x86_64 Debian base, reproducing the
environment the runtime needs, so the project is created natively and
run inside the container.  `airpuls-sdk doctor` reports which half is
present, and `xapp run` explains the missing runtime rather than
failing on an import error.

The container is built for the host's architecture, so it is native
on an Apple-silicon Mac as well as on x86_64, and the runtime installs
in both — the whole workflow runs inside it.  The `PLATFORM` build
argument in the generated `.devcontainer/devcontainer.json` forces a
specific architecture when one is needed, at emulation speed.

## Platform support

| | Linux | macOS | Windows |
|---|---|---|---|
| install, `xapp new`, `validate`, `info`, `build`, `add sink`, `ric`, `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 build --docker-image` | needs docker | needs docker | needs docker |

`xapp deploy` builds the launcher's image, so it needs docker as
well — except with `--no-image`, which uploads the entry alone.

The deployment commands drive the OpenSSH tools.  Windows ships an
OpenSSH client, so key-based authentication works, but three things do
not: the interactive password fallback (it uses the POSIX
`SSH_ASKPASS` mechanism), the one-time `ssh-copy-id` offer, and the
restrictive mode applied to a fetched client key.  A Windows user
therefore needs a key the deployment host already trusts.  CI runs the
suite on Linux; macOS is covered by development use, and Windows is not
exercised at all.

## Quick start

```bash
airpuls-sdk xapp new my-xapp        # interactive wizard; --defaults for CI
                                    # (picks the RIC — endpoint, connection and
                                    # credentials come from it — generates,
                                    # git-inits, and provisions .venv;
                                    # --ric NAME skips the question, --no-venv
                                    # the environment)
cd my-xapp
airpuls-sdk xapp validate           # xapp.yml against xapp.schema.json
airpuls-sdk xapp build              # per-language build, output captured
airpuls-sdk xapp test               # run the project's tests/
airpuls-sdk xapp run                # foreground run against the RIC — after the readiness check, offering each fix in turn
airpuls-sdk xapp check              # ready to run? endpoint, transport, TLS material, sink credentials — and the fix per row
airpuls-sdk xapp run --docker       # the same from the built image, mounts done for you; --build builds it first
airpuls-sdk xapp build --docker-image  # the image; --tag names it, --base-image swaps the SDK base
airpuls-sdk xapp info               # project summary
airpuls-sdk xapp add sink --file …  # configure a telemetry destination
airpuls-sdk xapp connect            # connect the project to a registered RIC (endpoint, binding, credentials)
airpuls-sdk xapp deploy             # build + push the image, entry → the host
airpuls-sdk xapp config render --deploy  # the deploy template deploy uploads
airpuls-sdk xapp undeploy           # remove the entry from the host
airpuls-sdk xapp credentials        # register identity + fetch mTLS material and sink credentials
airpuls-sdk ric add lab …    # register a RIC (several allowed)
airpuls-sdk testbed up              # RIC on this Linux machine with the project's SM plugins; restarts on rebuild
airpuls-sdk testbed licence fingerprint  # what a testbed licence is issued for; `airpuls-sdk testbed licence install FILE` installs it
airpuls-sdk doctor                  # environment diagnostics
```

Inside an xApp project `doctor` also reports the project environment:
whether `.venv` is provisioned, and whether `requirements.txt` and
`tests/requirements.txt` are satisfied by what is installed there — a
fresh clone warns until `./bootstrap-env.sh` or `airpuls-sdk xapp venv`
runs.

A clone starts without the CLI, so every generated project ships
`bootstrap-env.sh`, a bash script that checks the prerequisites (Python
3.9+ with its `venv` module and `ensurepip`, the runtime wheel's
platform, docker and ssh as optional), names the system package to
install for anything missing without installing it, creates `.venv`,
installs the CLI and — through `xapp venv` — the project's
requirements, then runs `doctor` and `xapp validate`, runs `xapp test`
where the runtime is installed, and prints the commands up to the
first `xapp run`.  `--check` reports the prerequisites alone; sourced
(`source ./bootstrap-env.sh`) it ends with `.venv` active in the shell.

`xapp build --docker-image` and `xapp deploy` build the project's image on the
`airpuls/airpuls-xapp-sdk` base image, checking exactly the reference
the project's Dockerfile FROM names (default `latest`;
`xapp new --base-tag` pins an older release).  An absent base image is
reported with its remedy instead of surfacing as a `docker build`
failure, and on a terminal the command offers to pull it from Docker
Hub.  Declining leaves the `docker pull` in the message, next to the
alternative of building it from an airpuls-ric checkout.

`airpuls-sdk --install-completion` installs the shell hook (bash,
zsh, fish, PowerShell): commands, options and paths complete through
the parser, the registered RIC names complete wherever a command takes
one, and the scaffold's choice sets complete on `xapp new`.  The
callbacks read local state only — a completion never touches the
network.

## Telemetry sinks

An xApp emits into two channels — *timeseries* for numeric samples and
*structured* for event records — and `xapp add sink` configures a
destination for them in `xapp.yml`:

```bash
airpuls-sdk xapp add sink                              # asks what to add
airpuls-sdk xapp add sink --file ./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
```

Invoked without a destination on a terminal, the command asks which one
to add and for the values it needs, each with its default in brackets.
The InfluxDB organization and bucket default to what the deployment
provisions — its InfluxDB setup creates a fixed `xapp_metrics` bucket
for xApps beside the RIC's own — so a URL alone is a complete
destination; `--org` and `--bucket` override them.

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

Credentials never appear on the command line or in the manifest:
`--token-env NAME` (InfluxDB, default `INFLUX_TOKEN`) and
`--password-env NAME` (Redis) record the *name* of the environment
variable holding the secret.  A secret typed as an argument would be
kept in the shell history and written into `xapp.yml`, which is
committed with the project and uploaded to the deployment host, so a
value that is not a variable name is refused rather than stored — and
the prompts ask for the variable name too, never the secret.  Add a
destination without switching it on with `--disabled`.

## Deployment

Deployment follows the nrtric-ctrl pattern: every generated project
includes an executable `<name>.xapp` docker-run launcher, and
`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 a registered host.  The project
source is never uploaded; the image is the runtime artifact, and the
host pulls it.

`xapp.yml` is the developer's working file — the RIC they reach, their
sinks, their paths — and every local command uses it as-is.  The
uploaded `xapp.yml` is the deploy template the deployment renders
per run, derived from that file by a fixed mapping: the endpoint host
becomes `{{ RIC_IP }}` (scheme and port kept — the deployment switches
the scheme to the listener's), the `ric.tls` material paths become the
`{{ DEFAULT_*_FILE_PATH }}` tokens, `xapp.instance` becomes
`{{ INSTANCE_ID }}`, a sink URL on the RIC host or on the loopback
dials `{{ RIC_IP }}`, a relative file-sink path moves onto
`/xapp/data` (the directory the launcher mounts from the entry), and
the Redis and InfluxDB credentials name `REDIS_PASSWORD` and
`INFLUX_TOKEN`, the variables the deployment injects.  Everything
else is carried verbatim, comments included; a sink dialling another
host or an absolute path is kept and named.  `xapp config render
--deploy` prints that template (`--output FILE` writes it).

The image is tagged with the launcher's own reference, so what is
pushed is what the host resolves.  That reference must name a registry
— `registry.airpuls.de/xapps/<name>:latest`, given by `--image` on
`xapp new`.  The scaffold default (`<name>-<lang>:latest`) names none,
so it would push to Docker Hub; `xapp deploy` refuses it and says so.
When the registry rejects the push for want of credentials, the error
names the `docker login` to run.

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 be built for the host's architecture, a mismatch is refused |
| `--no-image` | upload the entry alone, building and publishing nothing — a configuration-only redeploy |

Several RICs can be registered (`ric add/list/use/remove`);
operations pick one with `--ric`, the registry default, or an
interactive list.  Transfers use the OpenSSH tools: key authentication
first (an identity file can be stored per RIC or passed per command),
and when the host denies it on an interactive terminal the CLI asks for
the SSH password — masked, held only in process memory for that
invocation, fed to OpenSSH through askpass so it never reaches a
command line or a log — while `ric add` offers a one-time
`ssh-copy-id` that makes every later connection passwordless.

## TLS

tls:// is first-class through the whole flow, and the RIC's own
listener configuration is the source of truth for the transport:

- `xapp new` against a RIC whose listener is tls:// emits the
  `ric.tls` file set the schema requires, at the paths the launcher
  mounts, with the listener identity as `server_name`, and registers
  the xApp's identity with that RIC at creation; `--endpoint tls://…`
  by hand does the same once exactly one registered RIC dials that
  host, and otherwise leaves the registration to `xapp credentials`.
- `xapp deploy` reads the RIC's listener configuration before
  uploading.  Against a tls:// listener it generates the client
  certificate (CN = deployment name, the RIC's identity binding) and
  places CA, certificate, and key beside the deployed launcher; the
  template keeps the project's scheme and the deployment switches it
  to tls:// when it renders the entry, so a scheme mismatch cannot be
  deployed.  Against a plaintext listener a tls:// project is refused:
  the CLI never downgrades a secured configuration.
- `xapp connect` connects the project to a registered RIC: it writes
  `ric.endpoint` and, against a tls:// listener, reads that listener's
  server certificate to record 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: when it fails the endpoint is
  still written, with a warning).  It records the RIC as the project's
  RIC in the gitignored `.airpuls/ric.yml` — `credentials`, `deploy`,
  `undeploy` and the run's own checks use it without asking; `--ric`
  overrides per command — and syncs what a local run needs from it:
  the TLS identity and material for a tls:// listener, the sink
  credentials for either transport (`--no-sync` writes the endpoint
  alone).  A connected project keeps itself in sync: `xapp add sink`
  fetches the credential a new sink reads, and a run that finds sink
  credentials or TLS material missing fetches them from the project's
  RIC before launching.  The deploy template carries a recorded
  identity verbatim; the deployment's own dial name matches the RIC
  certificate, so an entry without one verifies as well.
- `xapp credentials` registers the identity and fetches CA + client
  pair into the project's `certs/` directory, which `xapp run` picks up
  automatically.  It also reads the deployment's `.env` on the host
  and writes the Redis password and InfluxDB token to the project's
  `.airpuls/sinks.env` (owner-only; `REDIS_PASSWORD`, `INFLUX_TOKEN`),
  which `xapp run` and `xapp run --docker` load — a variable set in
  the shell wins.  `--no-sinks` leaves them on the host.  That file is
  the deployment's secrets in clear text on a developer machine:
  development environments only, never a production RIC, never
  committed — the scaffold's `.gitignore` excludes `.airpuls/` and
  `certs/`; `xapp.yml` stays committed and names only the variables.

## Output

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: 1 generic, 2 usage,
3 validation, 4 subprocess, 5 toolchain/environment.

### JSON output for editor integrations

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

```bash
airpuls-sdk --json xapp info
airpuls-sdk --json doctor          # one record per environment check
airpuls-sdk --json ric list # registered targets, for a picker
```

```json
{"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` — and the process exit status is the same
in both modes, so a caller can branch on the status and read 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 and required flags — so an integration can
build 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 instead of terminal lines, and the
streamed output of `xapp run` moves to stderr, keeping stdout a single
document; its transcript path is in `data.log`.

## Development

The suite runs without the xApp runtime and without a native build:

```bash
pip install -e .[test]
pytest tests
```
