dapcli
dapcli is a headless CLI for the Dell Automation Platform orchestrator that drives the REST API directly so blueprints, deployments, and secrets can be managed from a script 1. It fills the gap left by the portal web UI and Dell’s own Python SDKs, which let the orchestrator drive Ansible playbooks and Terraform plans, not the other way around.
The tool orchestrates authentication, generic REST interactions, and specialized plugin-based infrastructure management through a modular architecture. It supports two authentication paths: an interactive OIDC handshake that persists session cookies, and a non-interactive client-credentials flow that mints Bearer tokens for CI or headless automation 2. These tokens are then used by the generic REST client to interact with the platform’s v3.1 API endpoints for blueprints, secrets, and inventory 3.
| Subsystem | Description |
|---|---|
| CLI Interface | Top-level Click group registration and global flag handling 1. |
| Authentication | OIDC token mint, browser login, and client-credentials exchange 4. |
| REST API Layer | Generic v3.1 REST client handling bearer token injection and request execution 3. |
| Plugin Subsystems | Specialized command groups for blueprints, secrets, inventory, and deployments 1. |
# dapcli
> **Disclaimer: unofficial and unsupported.** Provided for testing and
> evaluation only, on an "AS IS" basis, with no warranty and no support. Not
> affiliated with or endorsed by Dell. See [DISCLAIMER.md](DISCLAIMER.md).
A headless CLI for the Dell Automation Platform orchestrator. Drives
the REST API directly so blueprints, deployments, and secrets can be
managed from a script.
## Why
The portal web UI is the only first-class client shipped with the
platform. Dell's own Python SDKs and Terraform providers point the
opposite way: they let the orchestrator drive Ansible playbooks and
Terraform plans, not the other way around. There is no first-party
tool for an operator who wants to upload blueprints in bulk, rotate
secrets, or snapshot inventory from a script. `dap-cli` is that
tool.
## Prerequisites
- Python 3.14 or newer.
- Network reachability to the orchestrator portal
(`https://dap-portal.<domain>`).
- An admin username and password for the portal (the same credentials
used to sign in via the web UI), OR a customer-org admin client's
client_id + secret + org UUID.
## Install
```bash
python3 -m venv .venv
.venv/bin/pip install -e '.[test]'
.venv/bin/dap --help
```
## Configure and log in
Two commands. The first writes a `~/.config/dap-cli/config.json`
dap auth import-client-credentials \
--client-id 00000000-0000-0000-0000-000000000099 \
--org-id 00000000-0000-0000-0000-000000000001
# (prompts for client_secret with input hidden)
# CI variants:
pass show dap/client-secret | dap auth import-client-credentials \
--client-id 00000000-0000-0000-0000-000000000099 \
--client-secret-stdin
dap auth import-client-credentials \
--client-id 00000000-0000-0000-0000-000000000099 \
--client-secret-file /run/secrets/dap_client_secret
```
The credentials persist to `~/.config/dap-cli/api-client.json` at
mode 0600. From that point on, the same `dap blueprint ...`,
`dap secret ...`, `dap inventory ...`, `dap deployment ...`
commands work without a portal session.
On rare DAP 2.0.0.1 installs the portal's OIDC callback is broken
upstream (see `docs/chart-bugs.md`). For those clusters,
`dap auth bootstrap-service-account` provisions the admin client
via the iam-mgmt-svc database directly. Most operators never need
this path.
## Layout
```
dap-cli/
+-- src/dap/
| +-- cli.py top-level Click group registration
| +-- secret_input.py shared `read_secret` helper (TTY prompt /
| | stdin / file). Every subcommand that takes
| | a secret routes through here.
| +-- orch_client.py v3.1 REST client (Cookie: session.id auth)
| +-- config/ `dap config ...`, realm auto-discovery
| +-- auth/ `dap auth login` (browser OIDC),
| | `dap auth import-client-credentials`,
| | `dap auth mint-token`, session persistence
"""Token acquisition for the generic ``/rest/v1`` command layer.
DAP authenticates ``/rest/v1`` calls with a raw bearer token in the
``Authorization`` header (not ``Authorization: Bearer ...`` -- just the token).
Tokens are obtained one of two ways:
- a persisted raw token (``dap config set-credentials --token`` /
``dap tokens api-token --save``);
- an OAuth2 ``client_credentials`` exchange at
``{portal_server}/rest/v1/oidc/token``. The portal is the OIDC issuer; the
orchestrator accepts portal-issued tokens for its own API even though its
own token endpoint rejects the client-credentials grant.
The bearer is then reused for every request until it 401s, at which point the
caller re-mints.
"""
from __future__ import annotations
import base64
import json
from dataclasses import dataclass
from typing import Any
import httpx
from ..errors import DapError
from .config import Binding, Config, resolve_binding
from .credentials import CredentialStore
def decode_claims(token: str) -> dict[str, Any]:
"""Best-effort decode of a JWT payload (no signature verification)."""
if token.count(".") < 2:
return {}
payload = token.split(".", 2)[1]
payload += "=" * (-len(payload) % 4)
try:
return json.loads(base64.urlsafe_b64decode(payload))
except ValueError, json.JSONDecodeError:
"""Authentication public surface."""
from __future__ import annotations
from .browser import browser_login
from .session import (
Session,
build_session,
load_session,
save_session,
session_path,
)
from .token import AccessToken, mint_onprem_token
__all__ = [
"AccessToken",
"Session",
"browser_login",
"build_session",
"load_session",
"mint_onprem_token",
"save_session",
"session_path",
]