Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

raptor logo

raptor

raptor is a hawkBit-compatible over-the-air (OTA) update server, written in Rust. It speaks hawkBit’s DDI v1 device API — so SWUpdate, the RAUC hawkbit-updater, and other hawkBit clients work unchanged — and the core hawkBit Management API workflow, all from a single static binary and one config file.

Where a stock hawkBit deployment is Java + a relational database + RabbitMQ, raptor is one process backed by SQLite or Postgres. No broker, no JVM, no servlet container.

Highlights

  • Drop-in DDI v1 — devices poll, download, and report feedback exactly as they do against hawkBit.
  • Core Management API — targets, software modules, artifacts, distribution sets, assignments, actions, rollouts, and target filters, with hawkBit-shaped JSON, paging, sorting, and FIQL q= filtering.
  • Rollouts — staged, threshold-driven group deployments.
  • Target filters + auto-assignment — saved FIQL queries that assign a distribution set to matching devices automatically.
  • Confirmation flow — optional device/operator confirmation before a deployment starts.
  • One binary — an embedded web console ships inside the executable; SQLite by default, Postgres when you need it.
  • Packaged — a Debian package with a hardened systemd unit.

Where to go next

Note: raptor is young and its surface is a subset of hawkBit’s. The compatibility matrix is the source of truth for what exists today.

What is raptor?

raptor is a Rust reimplementation of the Eclipse hawkBit server, targeting drop-in API compatibility with the parts of hawkBit that device fleets use every day.

The problem it solves

hawkBit is the de-facto open-source backend for embedded/IoT software updates. Its device-facing DDI protocol is spoken by mature clients — SWUpdate’s suricatta module, the RAUC hawkbit-updater, and Collabora’s hawkbit Rust crate. But a production hawkBit deployment brings a heavy operational footprint: a JVM, a servlet container, a relational database, and (for the DMF device path) RabbitMQ.

raptor keeps the API and drops the weight:

hawkBitraptor
RuntimeJVM / Spring Bootsingle static binary
DatabaseMySQL / PostgresSQLite or Postgres
Message brokerRabbitMQ (DMF)none
Device APIDDI v1 + DMFDDI v1
Deploymentmulti-serviceone process, one config file

Because raptor implements hawkBit’s DDI v1 contract field-for-field, existing devices do not know the difference — you point them at raptor’s URL and they poll, download, and report just as before.

What raptor is

  • A device update server: it tells devices what to install, serves the artifacts, and records their feedback.
  • A fleet management API: create targets, upload firmware, compose distribution sets, assign updates, and run staged rollouts over REST.
  • A single-fleet, single-tenant server. It accepts (and ignores) hawkBit’s tenant URL segment, so clients configured for a tenant still work, but there is no tenant isolation. Run one raptor per fleet.

What raptor is not (yet)

raptor implements a growing subset of hawkBit. It does not currently provide the DMF (AMQP) device path, multi-tenancy, tags, target types, metadata endpoints, maintenance windows, or the full set of action types. See hawkBit Compatibility for the authoritative list.

Design principles

  • Compatibility is the contract. Clients branch on HTTP status codes and JSON field names; raptor matches hawkBit’s exactly, backed by golden-fixture and end-to-end tests against a real hawkBit client.
  • One binary, one config file. Everything — including the web console — ships in the executable. Configuration is a single TOML file with environment overrides.
  • Boring persistence. One SeaORM entity/migration set targets both SQLite and Postgres; migrations run automatically at startup.

Quick Start

This gets a raptor server running and deploys an update to a (virtual) device in a few commands. For production installs, see Installation.

1. Build and configure

$ cargo build --release
$ printf 'yourpassword\n' | ./target/release/raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...

Put that hash into a raptor.toml:

bind = "0.0.0.0:8088"
database_url = "sqlite://raptor.db?mode=rwc"   # or postgres://user:pass@host/db
artifact_dir = "./artifacts"

[ddi]
anonymous = true          # dev only — no device auth

[mgmt]
username = "admin"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."

Warning: anonymous = true disables all device authentication. Use it for local experiments only. See Authentication for production setups.

2. Run the server

$ ./target/release/raptor serve --config raptor.toml
raptor listening bind=0.0.0.0:8088

The database is created and migrated automatically on first start.

3. Deploy an update

Using the Management API (HTTP Basic with the admin credentials):

# 1. a software module
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules \
  -H 'Content-Type: application/json' \
  -d '[{"name":"rootfs","version":"1.0","type":"os"}]'

# 2. an artifact on module 1
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
  -F 'file=@rootfs.img'

# 3. a distribution set bundling module 1
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/distributionsets \
  -H 'Content-Type: application/json' \
  -d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'

# 4. assign DS 1 to a device (auto-registered on first poll)
curl -u admin:yourpassword -X POST localhost:8088/rest/v1/targets/my-device/assignedDS \
  -H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'

4. Poll as the device

$ curl localhost:8088/DEFAULT/controller/v1/my-device
{"config":{"polling":{"sleep":"00:05:00"}},
 "_links":{"deploymentBase":{"href":".../deploymentBase/1"},
           "configData":{"href":".../configData"}}}

The deploymentBase link tells the device an update is waiting. From here a real hawkBit client downloads the artifacts and posts feedback. Walk through the full cycle in Your First Deployment.

hawkBit Compatibility

raptor implements a subset of hawkBit, chosen to cover the device update workflow first. This page is the source of truth for what exists today.

Device API (DDI v1)

The DDI v1 contract is implemented field-for-field and verified with golden fixtures and an end-to-end test against a real hawkBit DDI client.

FeatureStatus
Poll root, config.polling.sleep, _links
deploymentBase (download/update modes, chunks, artifacts)
Deployment feedback state machine
cancelAction + cancel feedback
configData (attributes: merge / replace / remove)
installedBase
Artifact download with HTTP Range (resume)
.MD5SUM companion endpoint
Auto-registration (gateway token / anonymous)
confirmationBase confirmation flow
Maintenance windows
DMF (AMQP) device path

Management API

AreaStatus
Targets CRUD, assignedDS, installedDS, actions, attributes
Software modules CRUD + multipart artifact upload/list/download/delete
Distribution sets CRUD + module composition
Actions (per-target and fleet-wide list/filter)
Rollouts (create/start/pause/resume/delete, deploy groups)
Target filters + auto-assignment
Per-target auto-confirm
Paging (offset/limit), sort=, q= FIQL on lists
Software-module / distribution-set / target types CRUD (composition drives complete; target-type/DS-type compatibility enforced)
Target / distribution-set tags CRUD, assign/unassign, q=tag==x
Metadata endpoints (targets / modules / DS, targetVisible on module entries)
All four action types + force escalation (PUT .../actions/{id}) and force-quit (DELETE ...?force=true)
Rollout approval workflow, dynamic rollouts
Maintenance windows
Multi-assignment / action weights

Action types

hawkBit has forced, soft, downloadonly, and timeforced. raptor models all four. timeforced starts out soft and escalates once its deadline passes; downloadonly forces the download and never asks the device to install. An operator can escalate a running action to forced with PUT /rest/v1/targets/{controllerId}/actions/{actionId}, and force-quit one that the device is not acknowledging with DELETE …/actions/{actionId}?force=true.

Auth

MechanismStatus
DDI target security token
DDI shared gateway token
DDI anonymous mode
Management API HTTP Basic (single admin)
Session cookie for the web console
mTLS / certificate DDI auth
Multiple users / roles, OIDC

Tenancy

raptor is single-tenant. The DDI URL’s /{tenant}/controller/v1/... segment is accepted and ignored; all generated links use the tenant name DEFAULT. There is no per-tenant data isolation — run one raptor instance per fleet.

Note: Items marked ❌ are tracked as issues on the GitHub repository. The schema is designed so these can be added without breaking existing deployments.

Installation

raptor is a single binary. You can build it from source or install the Debian package.

Debian / Ubuntu package

Prebuilt .deb packages are attached to each GitHub Release:

$ sudo dpkg -i raptor_*.deb

The package installs:

  • /usr/bin/raptor — the server, with the web console embedded
  • /etc/raptor/config.toml — default config, registered as a dpkg conffile (your edits survive upgrades)
  • /usr/lib/systemd/system/raptor.service — a hardened systemd unit running as a transient DynamicUser, with state (SQLite DB + artifacts) under /var/lib/raptor

The service is enabled but not started on install, because you must set an admin password first:

$ raptor hash-password                 # type a password, copy the hash
$ sudoedit /etc/raptor/config.toml     # paste into password_hash, pick DDI auth
$ sudo systemctl start raptor

See Running as a systemd Service for the unit’s hardening details and how to supply secrets safely.

From source

You need a recent stable Rust toolchain.

$ git clone https://github.com/rosterloh/raptor
$ cd raptor
$ cargo build --release
$ ./target/release/raptor --version

The binary lands at target/release/raptor.

Building with the web console

The embedded web console is behind the embed-ui Cargo feature. It requires the Dioxus CLI (dx) to build the WASM bundle first:

$ cargo binstall dioxus-cli@0.7.10
$ dx build --release --package raptor-ui
$ cargo build --release --features embed-ui

Without embed-ui, the server runs identically but does not serve /ui.

Building the Debian package yourself

$ cargo install cargo-deb
$ dx build --release --package raptor-ui      # populate the embedded UI
$ cargo deb -p raptor                         # -> target/debian/raptor_*.deb

Database

raptor supports SQLite and Postgres, selected by the database_url scheme. Migrations run automatically at startup, so there is no separate migrate step.

  • SQLite: sqlite://raptor.db?mode=rwc (the mode=rwc creates the file)
  • Postgres: postgres://user:pass@host/dbname

Next steps

Configuration

raptor reads a single TOML file (default raptor.toml, override with --config). Every key can also be set via a RAPTOR_* environment variable, which is useful for secrets and containers.

Minimal config

bind = "0.0.0.0:8088"
database_url = "sqlite://raptor.db?mode=rwc"   # or postgres://user:pass@host/db
artifact_dir = "./artifacts"

[ddi]
gateway_token = "change-me"        # or anonymous = true for dev

[mgmt]
username = "admin"
password_hash = "$argon2id$..."    # from `raptor hash-password`

Only database_url, artifact_dir, and the [mgmt] credentials are required; everything else has a default. See the Configuration Reference for every key and its default.

Environment overrides

Any key maps to an environment variable prefixed with RAPTOR_, with nested tables joined by a double underscore (__):

export RAPTOR_BIND="127.0.0.1:9090"
export RAPTOR_DDI__ANONYMOUS=true
export RAPTOR_DDI__GATEWAY_TOKEN="super-secret"
export RAPTOR_MGMT__PASSWORD_HASH='$argon2id$...'

Environment values override the TOML file. This is the recommended way to inject secrets — a plaintext gateway token in a world-readable config file is a liability (the Debian package addresses this with a root-only raptor.env; see Running as a systemd Service).

Generating the admin password hash

The Management API and web console authenticate against a single admin credential stored as an argon2id hash:

$ printf 'yourpassword\n' | raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...

Copy the output into mgmt.password_hash.

Key groups at a glance

SectionPurpose
top levelbind, database_url, artifact_dir, max_artifact_size, url, rollout_eval_interval_secs
[ddi]device-facing auth and polling: anonymous, gateway_token, polling_interval, confirmation_flow
[mgmt]Management API / web console admin: username, password_hash

The url key

By default the _links in API responses are derived from the incoming request’s Host header. If raptor sits behind a reverse proxy that rewrites the host, set url to the externally visible base so devices receive dialable links:

url = "https://raptor.example.com"

Your First Deployment

This walks through a complete update cycle: create the content, assign it, and watch a device install it. It uses curl for both the operator (Management API) and the device (DDI API) sides.

Assume raptor is running on localhost:8088 with admin admin:yourpassword and ddi.anonymous = true (so we can poll without a device token).

1. Create a software module

A software module is a named, versioned unit of a given type (os, firmware, runtime, or application).

curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules \
  -H 'Content-Type: application/json' \
  -d '[{"name":"rootfs","version":"1.0","type":"os"}]'
# -> [{"id":1, ...}]

2. Upload an artifact

Artifacts are uploaded as multipart form data. raptor streams the bytes to disk and computes the SHA-1, MD5, and SHA-256 hashes as it goes.

curl -u admin:yourpassword -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
  -F 'file=@rootfs.img'
# -> {"id":1,"providedFilename":"rootfs.img","size":..., "hashes":{...}}

3. Compose a distribution set

A distribution set (DS) bundles one or more modules into a releasable unit. A DS is complete once it has the modules its type requires.

curl -u admin:yourpassword -X POST localhost:8088/rest/v1/distributionsets \
  -H 'Content-Type: application/json' \
  -d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'
# -> [{"id":1,"complete":true, ...}]

4. Assign the DS to a target

You do not need to create the target first — an unknown controller ID is auto-registered on its first poll. Assigning creates an action (the record of one deployment).

curl -u admin:yourpassword -X POST localhost:8088/rest/v1/targets/my-device/assignedDS \
  -H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'
# -> {"assigned":1,"alreadyAssigned":0,"total":1,"assignedActions":[{"id":1}]}

5. Device poll

The device polls the DDI root. Because an action is pending, the response carries a deploymentBase link.

curl localhost:8088/DEFAULT/controller/v1/my-device
# _links.deploymentBase -> .../deploymentBase/1

6. Fetch the deployment

curl localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1

The response describes the download/update modes and lists each module’s artifacts with hashes, sizes, and download links. A real client (SWUpdate, RAUC) downloads the artifacts from those links (which support HTTP Range for resume).

7. Report feedback

The device reports progress, then a final result. Feedback drives the action state machine.

# progress
curl -X POST localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1/feedback \
  -H 'Content-Type: application/json' \
  -d '{"status":{"execution":"proceeding","result":{"finished":"none"}}}'

# success
curl -X POST localhost:8088/DEFAULT/controller/v1/my-device/deploymentBase/1/feedback \
  -H 'Content-Type: application/json' \
  -d '{"status":{"execution":"closed","result":{"finished":"success"}}}'

On closed/success the action becomes finished and the target’s updateStatus becomes in_sync. A subsequent poll no longer offers a deploymentBase, and installedBase now reflects the installed DS.

8. Verify

curl -u admin:yourpassword localhost:8088/rest/v1/targets/my-device
# "updateStatus": "in_sync"

curl -u admin:yourpassword localhost:8088/rest/v1/targets/my-device/actions
# the action is "finished"

That’s a full cycle. From here, explore Rollouts to stage a deployment across many devices, or Target Filters to assign automatically.

Targets & Auto-Registration

A target is a device raptor can update, identified by a unique controllerId. Targets carry a security token, a reported set of attributes, and an updateStatus.

Update status

Every target has an updateStatus reflecting where it is in the update cycle:

StatusMeaning
unknowncreated via the Management API, never polled
registeredknown to the server, no update assigned
pendingan update is assigned and in progress
in_syncrunning the assigned distribution set
errorthe last deployment failed

Creating targets

Explicitly (Management API)

curl -u admin:pw -X POST localhost:8088/rest/v1/targets \
  -H 'Content-Type: application/json' \
  -d '[{"controllerId":"device-42","name":"Device 42"}]'

The request body is an array, so you can create many at once. A securityToken is generated if you don’t supply one.

Automatically (auto-registration)

An unknown controllerId that polls the DDI API is created on the spot with status registered — hawkBit’s plug-and-play behavior. Auto-registration requires the poll to be authenticated by the shared gateway token, or DDI anonymous mode to be on. See Authentication.

Listing and filtering

The list endpoint supports paging, sorting, and FIQL:

curl -u admin:pw 'localhost:8088/rest/v1/targets?offset=0&limit=50&sort=controllerId:ASC'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=updateStatus==error'

Filterable fields include controllerId (alias id), name, description, updateStatus, lastControllerRequestAt, and address. See Filtering with FIQL.

Last-seen address

A target’s address / ipAddress is recorded from its DDI polls, so a device that self-registers shows one without any Management API call. By default it’s the socket peer address.

Behind a reverse proxy the socket peer is the proxy, so point raptor at the header carrying the real address:

[ddi]
trusted_proxy_header = "x-forwarded-for"

This is unset by default because a device can put whatever it likes in that header — only set it when a proxy you control is rewriting it. raptor reads the rightmost entry, the hop appended by the proxy directly in front of it; entries to the left are caller-supplied and so spoofable. Values that don’t parse as an IP are ignored. Both X-Forwarded-For lists and RFC 7239 Forwarded (for=…) syntax are understood, with or without a port.

Attributes

Devices report key/value attributes (hardware revision, OS version, …) via the DDI configData endpoint. Retrieve them with:

curl -u admin:pw localhost:8088/rest/v1/targets/device-42/attributes
# {"hw":"rev2","os":"linux"}

Attributes are set by the device, in three modes — merge (default), replace, and remove — described in the DDI API reference.

Note: target attributes (device-reported) are distinct from hawkBit metadata (operator-set key/value pairs), which raptor does not yet implement.

Poll status

When a target has polled at least once, its representation includes a pollStatus block with the last request time, the next expected request time (derived from the configured polling interval), and an overdue flag — handy for spotting devices that have gone quiet.

Software Modules & Artifacts

A software module is a named, versioned unit of updatable content. An artifact is a file belonging to a module (the actual firmware image, package, etc.).

Module types

raptor seeds four software-module types: os, firmware, runtime, and application. Types are read-only in raptor (created via migration); you reference them by key when creating a module.

curl -u admin:pw -X POST localhost:8088/rest/v1/softwaremodules \
  -H 'Content-Type: application/json' \
  -d '[{"name":"rootfs","version":"1.0","type":"os","vendor":"ACME"}]'

The combination of name + version + type is unique; a duplicate returns 409 Conflict.

Uploading artifacts

Artifacts are uploaded as multipart/form-data. raptor streams the upload to disk while computing the SHA-1, MD5, and SHA-256 hashes in one pass:

curl -u admin:pw -X POST localhost:8088/rest/v1/softwaremodules/1/artifacts \
  -F 'file=@rootfs.img'

The response includes the computed hashes and the byte size. Maximum upload size is governed by max_artifact_size (see the Configuration Reference).

Content-addressed storage

Blobs are stored once per SHA-256, laid out git-object-style:

<artifact_dir>/<sha256[0..2]>/<sha256>

Uploading identical content twice stores the bytes once and adds a second reference; the blob is removed from disk only when the last artifact row referencing it is deleted. This dedup is transparent — each module still sees its own artifact row with its own filename.

Listing, downloading, deleting

# list a module's artifacts
curl -u admin:pw localhost:8088/rest/v1/softwaremodules/1/artifacts

# download (operator side)
curl -u admin:pw -O localhost:8088/rest/v1/softwaremodules/1/artifacts/1/download

# delete
curl -u admin:pw -X DELETE localhost:8088/rest/v1/softwaremodules/1/artifacts/1

Device-side download

Devices fetch artifacts through the DDI API, not the Management API:

GET /{tenant}/controller/v1/{cid}/softwaremodules/{moduleId}/artifacts/{filename}

This endpoint supports HTTP Range requests (RFC 7233), so an interrupted download resumes rather than restarting — important for large firmware images over flaky links. A companion {filename}.MD5SUM endpoint returns the md5sum-file format some clients verify against.

Distribution Sets

A distribution set (DS) is the releasable unit you assign to devices: a named, versioned bundle of software modules.

DS types

raptor seeds three distribution-set types: os, os_app, and app. Like module types, they are read-only. The type determines which module types a DS needs to be considered complete.

Creating a distribution set

curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsets \
  -H 'Content-Type: application/json' \
  -d '[{"name":"release","version":"1.0","type":"os","modules":[{"id":1}]}]'

You can pass modules inline (as above) or add them afterward:

curl -u admin:pw -X POST localhost:8088/rest/v1/distributionsets/1/assignedSM \
  -H 'Content-Type: application/json' -d '[{"id":2}]'

Completeness

A DS is complete when it contains the modules its type requires. Only complete distribution sets can be assigned or deployed — assigning an incomplete DS (or attaching one as a target-filter auto-assignment) returns 400 Bad Request.

curl -u admin:pw localhost:8088/rest/v1/distributionsets/1
# {"id":1,"complete":true, "modules":[...], ...}

Listing and filtering

Distribution sets support the usual paging, sorting, and FIQL query parameters:

curl -u admin:pw 'localhost:8088/rest/v1/distributionsets?q=name==release*&sort=version:DESC'

Lifecycle

Distribution sets are referenced by actions and rollouts. raptor keeps the content-addressing scheme independent of DS identity, so the same artifact bytes can back many distribution sets without duplication.

Assignments & Actions

An action is the record of one deployment: a distribution set being rolled out to one target. Assigning a DS creates an action; the device’s feedback drives it to completion.

Assigning a distribution set

curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/assignedDS \
  -H 'Content-Type: application/json' -d '{"id":1,"type":"forced"}'

The type is the action type. All four of hawkBit’s are supported, and each maps to the download/update handling modes the device is given in deploymentBase:

typedownloadupdateMeaning
forced (default)forcedforcedinstall as soon as possible
softattemptattemptthe device may defer per its own policy
timeforcedattemptforcedattemptforcedsoft until forcetime, forced after
downloadonlyforcedskipfetch the artifacts, do not install

An unknown type is rejected with 400.

timeforced takes a forcetime (epoch millis) alongside the type:

curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/assignedDS \
  -H 'Content-Type: application/json' \
  -d '{"id":1,"type":"timeforced","forcetime":1767225600000}'

Before that instant the device sees attempt; after it, forced — no server-side job is involved, the mode is computed per request. Omitting forcetime means “already reached”, so the action behaves as forced immediately (matching hawkBit’s default of 0). Note the request body spells it all-lowercase forcetime while the action response uses forceTime; that asymmetry is hawkBit’s and raptor mirrors it.

A downloadonly action completes when the device reports downloaded feedback rather than closed. Because nothing was installed, the target’s installedDS is deliberately left untouched — only assignedDS reflects the distribution set. The action ends with status: finished and detailStatus: downloaded.

Escalating a running action

A soft or timeforced action can be pushed through immediately:

curl -u admin:pw -X PUT localhost:8088/rest/v1/targets/device-42/actions/7 \
  -H 'Content-Type: application/json' -d '{"forceType":"forced"}'

The next deploymentBase the device fetches carries forced. Escalating an action that is no longer active returns 410 Gone.

One active action per target

raptor enforces hawkBit’s default invariant: a target has at most one active action. Assigning a new DS to a target that already has an active action cancels the old one and starts the new deployment. (hawkBit’s opt-in multi-assignment mode with action weights is not implemented.)

Action states

StateactiveMeaning
wait_for_confirmationyesawaiting confirmation before deploying (see Confirmation Flow)
runningyesdevice has been told to deploy
cancelingyescancellation requested, awaiting device acknowledgement
cancelednocancellation confirmed (or forced)
finishednodeployment succeeded
errornodeployment failed

Each transition and every piece of device feedback appends an ActionStatus history row (with optional messages).

Inspecting actions

# all actions on one target (newest first)
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions

# a single action
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions/1

# fleet-wide, filterable
curl -u admin:pw 'localhost:8088/rest/v1/actions?q=detailStatus==error'

The action JSON exposes status (pending while active, else finished) and detailStatus (the fine-grained state from the table above).

Status history

Every state change an action goes through — assignment, each piece of device feedback, cancellation — is recorded as a status entry. List them with:

# chronological (oldest first); pass ?sort=id:DESC for newest first
curl -u admin:pw localhost:8088/rest/v1/targets/device-42/actions/1/status

Each entry has a type (the reported status, e.g. running, finished, canceled), any messages the device or server attached, and reportedAt. The list supports the usual offset/limit/sort paging.

Cancelling

# request cancellation (device must acknowledge)
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targets/device-42/actions/1

# force-cancel server-side (no device acknowledgement)
curl -u admin:pw -X DELETE 'localhost:8088/rest/v1/targets/device-42/actions/1?force=true'

A normal cancel moves the action to canceling and offers the device a cancelAction link; the device confirms via cancel feedback, moving it to canceled. A forced cancel closes the action immediately.

Installed vs assigned

  • GET /rest/v1/targets/{cid}/assignedDS — the DS currently assigned (what the device should run).
  • GET /rest/v1/targets/{cid}/installedDS — the DS the device last successfully installed.

Both return 204 No Content when there is nothing to report.

Rollouts

A rollout deploys a distribution set across many targets in stages, advancing from one group to the next only when success thresholds are met — so a bad update is caught on a small group before it reaches the whole fleet.

How it works

  1. You create a rollout from a FIQL target filter, a distribution set, and a number of groups. Matching targets are split evenly across the groups at creation time.
  2. Each group has a success threshold and an error threshold (percentages).
  3. Starting the rollout schedules the first group only — its targets get the DS assigned.
  4. A background evaluator watches each running group:
    • When the error threshold is reached, the group and rollout pause.
    • When the success threshold is reached, the group finishes and the next group is scheduled.
  5. When the last group finishes, the rollout is finished.

Creating a rollout

curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "fleet-1.1",
        "distributionSetId": 1,
        "targetFilterQuery": "controllerId==device-*",
        "amountGroups": 3,
        "successCondition": {"condition":"THRESHOLD","expression":"90"},
        "errorCondition":   {"condition":"THRESHOLD","expression":"20"}
      }'
  • amountGroups splits matching targets into that many groups.
  • successCondition.expression / errorCondition.expression are percentages (0–100). If errorCondition is omitted, the error threshold never trips.
  • type is the action type every action the rollout creates inherits — forced (default), soft, timeforced or downloadonly — with forcetime alongside it for timeforced. A staged download-then-install is therefore a downloadonly rollout followed by a forced one over the same filter. An unknown type is rejected with 400.

The rollout starts in ready.

Lifecycle operations

curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/start
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/pause
curl -u admin:pw -X POST localhost:8088/rest/v1/rollouts/1/resume
curl -u admin:pw -X DELETE localhost:8088/rest/v1/rollouts/1
  • startreadyrunning; schedules the first group.
  • pauserunningpaused; the evaluator ignores paused rollouts.
  • resumepausedrunning; re-evaluates immediately.
  • delete — cancels any active actions in the rollout and removes it.

Inspecting groups

# deploy groups with per-group status and target counts
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups

# one group
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups/5

# the controllerIds in a group
curl -u admin:pw localhost:8088/rest/v1/rollouts/1/deploygroups/5/targets

Tracking progress

Rollouts and groups both carry totalTargetsPerStatus, hawkBit’s breakdown of their targets by deployment outcome:

{
  "id": 1, "name": "fleet-1.1", "status": "running", "totalTargets": 9,
  "totalTargetsPerStatus": {
    "notstarted": 0, "scheduled": 3, "running": 3,
    "error": 1, "finished": 2, "cancelled": 0
  }
}
  • notstarted — the rollout has not been started, so nothing is deployed yet.
  • scheduled — the group is waiting its turn; raptor creates actions only when a group is scheduled, so these targets have no action yet.
  • running — an action is in flight (including canceling and, with the confirmation flow on, wait_for_confirmation).
  • finished / error / cancelled — the action’s terminal state.

A rollout’s counts are the sum of its groups’. The web console renders both as progress bars — see the Web Console guide.

Evaluator cadence

The background evaluator runs every rollout_eval_interval_secs seconds (default 5). Lower it for snappier progression in testing, raise it to reduce load on large fleets. See the Configuration Reference.

Note: hawkBit’s rollout approval workflow and dynamic rollouts (groups that keep absorbing newly-matching targets) are not yet implemented. Group membership is a static snapshot taken at creation time.

Target Filters & Auto-Assignment

A target filter is a saved FIQL query. On its own it’s a convenient, named way to select devices. Attach a distribution set to it, and it becomes an auto-assignment rule: matching targets receive that DS automatically — now, and as new matching devices appear.

Creating a filter

curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters \
  -H 'Content-Type: application/json' \
  -d '{"name":"beta-ring","query":"controllerId==beta-*"}'

The query is validated against the target field map at write time — an invalid FIQL expression returns 400 Bad Request, and a duplicate name returns 409 Conflict. Update, fetch, list, and delete follow the usual REST shape:

curl -u admin:pw -X PUT    localhost:8088/rest/v1/targetfilters/1 \
  -H 'Content-Type: application/json' -d '{"query":"name==beta-*"}'
curl -u admin:pw           localhost:8088/rest/v1/targetfilters
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targetfilters/1

Attaching an auto-assign distribution set

curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters/1/autoAssignDS \
  -H 'Content-Type: application/json' \
  -d '{"id":1,"type":"forced"}'
  • id — the distribution set to assign. It must be complete (400 otherwise).
  • typeforced (default) or soft.

Attaching immediately assigns the DS to every currently-matching target. Read or detach the attachment with:

curl -u admin:pw           localhost:8088/rest/v1/targetfilters/1/autoAssignDS   # -> the DS, or 204
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targetfilters/1/autoAssignDS

From the web console

Everything above is also available under Target filters in the web console:

  • New filter / Edit — name plus FIQL query. The query is checked against the live target list as you type, so the form shows how many targets it matches (or the parser’s complaint) before you save; the API’s 400/409 messages are shown against the query and name fields.
  • Auto-assign… — pick a distribution set and the action type (forced or soft), or Detach to remove the attachment. An incomplete set is reported inline rather than as a raw failure.
  • The Auto-assign column names the attached set, so you can see which filters are live rules at a glance.

When auto-assignment runs

A matching target receives the DS:

  • On registration — a device auto-registering via DDI that matches a filter is assigned in the same poll (its very first poll can already return a deploymentBase).
  • On attribute change — updating a target via configData re-evaluates the filters.
  • Periodically — a background sweep (sharing the rollout evaluator’s task) catches targets created through other paths.

Non-disruptive by design

Auto-assignment never disturbs work in flight. A matching target is skipped if it already has that DS assigned, or if it has any active action. So a device mid-deployment is never clobbered by an auto-assign rule; it’s picked up on a later sweep once it’s idle.

Note: FIQL auto-assignment matches on the standard target fields (controllerId, name, updateStatus, …). Matching on device-reported attributes is not yet supported.

Tags

A tag is a free-form label you attach to targets or distribution sets: beta, eu-west, kiosk, qa. Tags carry no behaviour of their own — they exist so you can organise a fleet and then select it again with the tag== FIQL term, in list queries, saved target filters and rollouts.

Target tags and distribution-set tags are separate namespaces: a beta target tag and a beta DS tag are unrelated tags.

Creating tags

The create body is an array, like the other Management API create endpoints:

curl -u admin:pw -X POST localhost:8088/rest/v1/targettags \
  -H 'Content-Type: application/json' \
  -d '[{"name":"beta","description":"early access","colour":"#00ff00"}]'

name is required and unique (409 Conflict on a duplicate); description and colour are optional. Distribution-set tags are identical, at /rest/v1/distributionsettags.

Fetch, update and delete follow the usual REST shape. PUT only changes the fields present in the body:

curl -u admin:pw           localhost:8088/rest/v1/targettags/1
curl -u admin:pw -X PUT    localhost:8088/rest/v1/targettags/1 \
  -H 'Content-Type: application/json' -d '{"colour":"#ff0000"}'
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1

Deleting a tag deletes its assignments, not the tagged entities. Removing the beta tag leaves every target that carried it in place, simply untagged.

Tag lists support the usual paging, sort= and q= parameters over id, name, description and colour:

curl -u admin:pw 'localhost:8088/rest/v1/targettags?q=name==be*&sort=name:ASC'

Assigning tags

Assign one target by controller id, or several at once with a JSON array body:

curl -u admin:pw -X POST localhost:8088/rest/v1/targettags/1/assigned/dev-1
curl -u admin:pw -X POST localhost:8088/rest/v1/targettags/1/assigned \
  -H 'Content-Type: application/json' -d '["dev-2","dev-3"]'

Assignment is idempotent — re-assigning an already-tagged target succeeds and changes nothing, so bulk calls are safe to retry. An unknown controller id fails the whole call with 404.

Unassign with the same paths and DELETE, and list what a tag holds with GET .../assigned (a paged list of full target objects, accepting offset, limit, sort= and q=):

curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1/assigned/dev-1
curl -u admin:pw -X DELETE localhost:8088/rest/v1/targettags/1/assigned \
  -H 'Content-Type: application/json' -d '["dev-2","dev-3"]'
curl -u admin:pw 'localhost:8088/rest/v1/targettags/1/assigned?limit=100'

The reverse lookup — the tags carried by one entity — is a raptor extension, since hawkBit only exposes the tag-to-entity direction:

curl -u admin:pw localhost:8088/rest/v1/targets/dev-1/tags
curl -u admin:pw localhost:8088/rest/v1/distributionsets/7/tags

Distribution-set tags work the same way, keyed by distribution set id:

curl -u admin:pw -X POST   localhost:8088/rest/v1/distributionsettags/1/assigned/7
curl -u admin:pw -X POST   localhost:8088/rest/v1/distributionsettags/1/assigned \
  -H 'Content-Type: application/json' -d '[8,9]'
curl -u admin:pw           localhost:8088/rest/v1/distributionsettags/1/assigned

In the web console

The web console has a Tags page with a tab per kind, where tags are created, recoloured and deleted, alongside a count of how many targets or sets carry each one. A target’s or distribution set’s own tags appear on its detail page, where Manage… assigns and unassigns them, and the targets and distributions lists gain a tag dropdown that filters the list to one tag — combined with whatever is in the search box.

Filtering by tag

tag is available as a FIQL field on /rest/v1/targets and /rest/v1/distributionsets:

curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag==beta'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag!=beta'
curl -u admin:pw 'localhost:8088/rest/v1/targets?q=tag=in=(beta,canary)'
curl -u admin:pw 'localhost:8088/rest/v1/distributionsets?q=tag==qa'

It accepts ==, !=, =in= and =out= (plus * wildcards on the tag name); the ordering operators return 400. Negation applies to the membership rather than the name, so tag!=beta means “not tagged beta” — a target that also carries stable is still excluded.

Because saved target filters compile the same field map, tag== works there too, which makes it a natural way to scope a rollout:

curl -u admin:pw -X POST localhost:8088/rest/v1/targetfilters \
  -H 'Content-Type: application/json' \
  -d '{"name":"beta-ring","query":"tag==beta"}'

Combine it with any other term to narrow further:

tag==beta ; updateStatus==error
tag==eu-* , tag==us-*

Confirmation Flow

By default an assignment becomes an active deployment immediately. With the confirmation flow enabled, a new assignment first enters a wait_for_confirmation state and does not deploy until it is confirmed — either by the device over DDI, or by an operator activating auto-confirm.

This mirrors hawkBit’s confirmation flow (confirmationBase), which clients like the RAUC hawkbit-updater support.

The client has to implement confirmationBase. With the flow on, a waiting target’s poll offers confirmationBase and no deploymentBase. A client that doesn’t parse that link — the mainline Zephyr hawkbit client (subsys/mgmt/hawkbit) ignores it entirely, as do older SWUpdate/RAUC setups — sees nothing it understands, so it polls forever and never installs, with no error reported anywhere. raptor logs a warning at startup when the flow is on. Use auto_confirm_default (below) or per-target auto-confirm for those devices.

Enabling the flow

It’s a server-wide toggle in the config:

[ddi]
confirmation_flow = true

When off (the default), behavior is exactly as before — assignments go straight to running.

What a device sees

With the flow on, a target’s poll offers a confirmationBase link instead of deploymentBase:

$ curl localhost:8088/DEFAULT/controller/v1/device-42
# _links.confirmationBase -> .../confirmationBase/7

GET .../confirmationBase/{actionId} returns the pending deployment (the same chunk/artifact shape as deploymentBase, under a confirmation key) so the device can decide. The device then confirms or denies:

# confirm -> action goes to running; next poll offers deploymentBase
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/7/feedback \
  -H 'Content-Type: application/json' \
  -d '{"confirmation":"confirmed","details":["operator approved"]}'

# deny -> action stays waiting (a "denied" ActionStatus row is recorded)
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/7/feedback \
  -H 'Content-Type: application/json' \
  -d '{"confirmation":"denied","details":["not now"]}'

A denied action remains in wait_for_confirmation; the device may confirm later, or an operator can cancel it.

Auto-confirm

A target can be set to auto-confirm, so assignments skip the wait state entirely. Toggle it from the Management API:

curl -u admin:pw localhost:8088/rest/v1/targets/device-42/autoConfirm
# {"active": false}

curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/autoConfirm/activate
curl -u admin:pw -X POST localhost:8088/rest/v1/targets/device-42/autoConfirm/deactivate

Or by the device itself over DDI:

curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/activateAutoConfirm
curl -X POST localhost:8088/DEFAULT/controller/v1/device-42/confirmationBase/deactivateAutoConfirm

Activating auto-confirm releases any already-pending actions on that target — they transition straight to running. New assignments to an auto-confirm target never enter the wait state.

Auto-confirm by default

To run the flow only for the devices that actually support it, have every new target start out auto-confirming and deactivate it on the ones you want to confirm explicitly:

[ddi]
confirmation_flow = true
auto_confirm_default = true

This applies to targets created either way — DDI self-registration or POST /rest/v1/targets. It only affects new targets; existing ones keep whatever auto-confirm state they have.

Operator confirm/deny

There is currently no per-action operator confirm/deny over the Management API — device-driven confirm/deny is DDI-only. The operator path is to activate auto-confirm on the target (which releases pending actions), or to cancel the action.

Filtering with FIQL

Every Management API list endpoint accepts a q= query in FIQL (Feed Item Query Language, the same dialect hawkBit uses, also called RSQL). raptor compiles it to a database query.

Grammar

Comparison operators:

OperatorMeaning
==equal (supports * wildcards → SQL LIKE)
!=not equal (supports * wildcards)
=lt=less than
=le=less than or equal
=gt=greater than
=ge=greater than or equal
=in=in a list: field=in=(a,b,c)
=out=not in a list

Logical operators:

  • ; — AND
  • , — OR
  • AND binds tighter than OR; use parentheses to group.

Wildcards: * in a value becomes a SQL LIKE wildcard, so controllerId==dev-* matches everything starting with dev-.

Examples

updateStatus==error
controllerId==beta-*
updateStatus=in=(pending,error)
name==prod-* ; updateStatus==in_sync
updateStatus==error , updateStatus==pending

URL-encode the query when passing it on the command line:

curl -u admin:pw 'localhost:8088/rest/v1/targets?q=updateStatus%3D%3Derror'

Filterable fields

Each resource exposes its own field map; an unknown field returns 400 Bad Request. Common maps:

  • targetsid/controllerId, name, description, updateStatus, lastControllerRequestAt, address, tag
  • distribution setsid, name, version, description, complete, tag
  • target tags / DS tagsid, name, description, colour
  • actionsid, active, detailStatus
  • rolloutsid, name, status
  • target filtersid, name

Boolean fields (e.g. active) accept true/false and compile to typed boolean comparisons.

The tag field

tag is not a column but a membership test against the entity’s tags, so it only accepts ==, !=, =in= and =out= — the ordering operators return 400. Negation applies to the membership, not the tag name: tag!=beta means “not tagged beta”, so a target tagged both beta and stable is excluded.

tag==beta                        # targets tagged beta
tag!=beta                        # targets not tagged beta
tag=in=(beta,canary)             # tagged with either
tag==beta ; updateStatus==error  # tagged beta and in error

The same term works on /rest/v1/distributionsets and inside saved target filter queries (and therefore rollout target filters).

Where FIQL is used

Beyond q= on list endpoints, the same grammar drives:

  • Rollout target selection (targetFilterQuery).
  • Target filter queries and their auto-assignment matching.

The query is validated when a rollout or target filter is created, so an invalid expression is rejected up front rather than failing silently later.

Authentication

raptor has two independent auth zones: the device-facing DDI zone and the operator-facing Management zone.

DDI zone (devices)

Devices authenticate on every DDI request with one of:

  • Target security tokenAuthorization: TargetToken <token>, matched against the target’s stored token. Each target has its own token (generated at creation, or supplied).
  • Gateway tokenAuthorization: GatewayToken <token>, a single shared token from config. A valid gateway token also enables auto-registration of unknown controller IDs.

Configure them under [ddi]:

[ddi]
gateway_token = "shared-secret-for-registration"

Anonymous mode

[ddi]
anonymous = true

This disables DDI authentication entirely — any controller ID can poll and register. It’s convenient for local development and should not be used in production. It is off by default.

Management zone (operators)

The Management API and web console authenticate against a single admin credential using HTTP Basic:

[mgmt]
username = "admin"
password_hash = "$argon2id$..."     # from `raptor hash-password`

The password is stored as an argon2id hash, never in plaintext. Generate it with:

$ printf 'yourpassword\n' | raptor hash-password

The web console also issues a session cookie (via POST /rest/v1/login) so browser users don’t send Basic credentials on every request.

Note: raptor has a single admin user in this version. Multiple users, roles, OIDC, and certificate-based (mTLS) DDI auth are not yet implemented.

Deployment guidance

  • Terminate TLS at a reverse proxy in front of raptor; the DDI tokens and Basic credentials are bearer secrets that must not travel in cleartext.
  • Set url to the externally visible base URL so _links are dialable through the proxy (see Configuration).
  • Prefer environment variables or the systemd raptor.env file for the gateway token and password hash rather than committing them to a shared config file (see Running as a systemd Service).

Using raptor with Zephyr

Zephyr’s mainline hawkBit client (subsys/mgmt/hawkbit) works against raptor’s DDI API unchanged. This page is the minimal wiring, plus the two footguns worth knowing before you flash a fleet.

Kconfig symbols below are from Zephyr’s subsys/mgmt/hawkbit/Kconfig; the compatibility matrix at the end records what the client actually exercises.

Minimal configuration

Device side (prj.conf):

CONFIG_HAWKBIT=y
CONFIG_HAWKBIT_SERVER="ota.example.com"
CONFIG_HAWKBIT_PORT=8088
CONFIG_HAWKBIT_TENANT="DEFAULT"
CONFIG_HAWKBIT_POLL_INTERVAL=5          # minutes; 1..43200

# Pick one auth mode (see below)
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=y
CONFIG_HAWKBIT_DDI_SECURITY_TOKEN="shared-registration-secret"

# If raptor is behind TLS
# CONFIG_HAWKBIT_USE_TLS=y

If CONFIG_HAWKBIT_SERVER is a hostname rather than an IP, the device also needs CONFIG_DNS_RESOLVER=y.

Server side (raptor.toml):

url = "http://ota.example.com:8088"

[ddi]
gateway_token = "shared-registration-secret"
polling_interval = "00:05:00"

Keep polling_interval and CONFIG_HAWKBIT_POLL_INTERVAL consistent — raptor advertises its value as config.polling.sleep (HH:MM:SS), which the client reads and honours, so the Kconfig value is really just the pre-first-poll default.

Choosing an auth mode

The client sends exactly one of two headers, selected at build time:

Kconfig choiceHeader sentraptor side
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=yAuthorization: GatewayToken <token>[ddi] gateway_token = "<token>"
CONFIG_HAWKBIT_DDI_TARGET_SECURITY=yAuthorization: TargetToken <token>the target’s own securityToken

Either way the token value goes in CONFIG_HAWKBIT_DDI_SECURITY_TOKEN.

Gateway token is one shared secret for the whole fleet and permits auto-registration: a device that polls with it is created on first contact. Good for bringing a fleet up, weaker blast radius if extracted from one device.

Target token is per-device, so the target must exist in raptor first, with a token you provision into the firmware:

curl -u admin:pw -X POST localhost:8088/rest/v1/targets \
  -H 'Content-Type: application/json' \
  -d '[{"controllerId": "device-42", "securityToken": "per-device-secret"}]'

See Authentication for the full picture. [ddi] anonymous = true disables DDI auth entirely — convenient for a bring-up on a lab network, never in production.

Footgun 1: the tenant must be DEFAULT

Zephyr’s CONFIG_HAWKBIT_TENANT defaults to "default", and raptor is single-tenant: it accepts any tenant segment but emits DEFAULT in every link it returns. Case doesn’t matter (default and DEFAULT are the same tenant here), but anything else leaves the device following hrefs that disagree with its own configuration, and would break outright if multi-tenancy ever lands. raptor logs a warning the first time it sees one. Set it to DEFAULT and move on.

Footgun 2: don’t enable the confirmation flow

The client parses exactly three _links keys from the base poll — deploymentBase, configData, cancelAction. It has no confirmationBase handling at all.

So with [ddi] confirmation_flow = true, a waiting target is offered only confirmationBase, the device sees no link it understands, and it polls forever without installing and without reporting an error. If you need the flow for other clients, keep Zephyr devices out of it with:

[ddi]
confirmation_flow = true
auto_confirm_default = true    # new targets auto-confirm

See Confirmation Flow.

Compatibility matrix

What the mainline Zephyr client uses, and raptor’s status for each. “Verified” means covered by raptor’s integration tests, including a JSON-contract test (zephyr_client_json_contract) pinning the exact response shape the client’s strict JSON descriptors require.

DDI featureZephyr clientraptor
Base poll config.polling.sleep (HH:MM:SS)reads and honours✅ verified
_links.deploymentBaseparsed✅ verified
_links.configDataparsed; uploads whenever present✅ verified — advertised only when attributes are wanted, so devices don’t re-upload every poll
_links.cancelActionparsed✅ verified
_links.confirmationBasenot parsed⚠️ raptor supports it; do not enable for Zephyr (footgun 2)
configData PUT, mode: "merge"sends on every poll carrying the link✅ verified
deploymentBase chunks/artifactsparsed✅ verified
Artifact hashes.sha256verified against flashed image✅ verified
Artifact _links.download-httpthe link it downloads from✅ verified — see download-http vs download
Range: resume (CONFIG_HAWKBIT_SAVE_PROGRESS)sends bytes=N-✅ verified — 206 + Content-Range
Feedback execution / resultclosed, proceeding, canceled, scheduled, rejected, resumed, none / success, failure, none✅ verified
Authorization: TargetToken / GatewayTokeneither, chosen at build time✅ verified
Multi-tenancyCONFIG_HAWKBIT_TENANT⚠️ single-tenant, DEFAULT only (footgun 1)

Verifying against a real device

raptor’s test suite drives its DDI API with the Rust hawkbit crate and the JSON-contract test above — not with the Zephyr client itself, which needs hardware or QEMU. For an end-to-end check, build Zephyr’s samples/subsys/mgmt/hawkbit sample against a raptor instance and watch the server log: a successful cycle is a base poll, a configData PUT, a deploymentBase GET, artifact GETs, then feedback with execution: "closed".

Running as a systemd Service

The Debian package installs a hardened systemd unit at /usr/lib/systemd/system/raptor.service. This guide covers how it’s wired and how to operate it.

Layout

PathPurpose
/usr/bin/raptorthe binary (web console embedded)
/etc/raptor/config.tomlconfig, a dpkg conffile (edits survive upgrades)
/var/lib/raptor/state: SQLite DB and artifact blobs
/etc/raptor/raptor.envoptional, root-only secrets (not shipped)

First start

The service is enabled but not started on install — you must set an admin password first:

$ raptor hash-password                 # type a password, copy the hash
$ sudoedit /etc/raptor/config.toml     # set password_hash, choose DDI auth
$ sudo systemctl start raptor
$ systemctl status raptor

State directory

The unit uses systemd’s DynamicUser=yes with StateDirectory=raptor. That means:

  • raptor runs as a transient, unprivileged user allocated at runtime — there is no raptor user to manage and no postinst chown.
  • /var/lib/raptor is created and owned by that user automatically, mode 0750.

The default config points both the database and the artifact store there:

database_url = "sqlite:///var/lib/raptor/raptor.db?mode=rwc"
artifact_dir = "/var/lib/raptor/artifacts"

/var/lib is the FHS location for persistent, service-owned state — the content-addressed artifact blobs are the source of truth (not regenerable cache), which is why they live here rather than under /var/cache.

Secrets

The config file is world-readable because the DynamicUser must read it. Keep plaintext secrets out of it. Put them in a root-only environment file, which systemd reads before dropping privileges:

$ sudo install -m 600 /dev/null /etc/raptor/raptor.env
$ echo 'RAPTOR_DDI__GATEWAY_TOKEN=super-secret' | sudo tee -a /etc/raptor/raptor.env
$ sudo systemctl restart raptor

The unit loads it via EnvironmentFile=-/etc/raptor/raptor.env (the leading - makes it optional). The mandatory password_hash is an argon2 hash and is safe to keep in the config.

Hardening

The unit ships with a broad systemd sandbox: ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, NoNewPrivileges=yes, MemoryDenyWriteExecute=yes, a restricted SystemCallFilter=@system-service, an empty CapabilityBoundingSet, and address-family restrictions. raptor needs no Linux capabilities because it binds a high port (8088) by default.

Note: If you change bind to a privileged port (< 1024), grant the capability explicitly with AmbientCapabilities=CAP_NET_BIND_SERVICE via a drop-in (systemctl edit raptor), or bind a high port and let the reverse proxy front it.

Logs

raptor logs to stdout/stderr, captured by the journal:

$ journalctl -u raptor -f

Adjust verbosity with the RUST_LOG env var (e.g. RUST_LOG=raptor=debug) via a systemctl edit drop-in or raptor.env.

Web Console

raptor ships an optional web console — a Dioxus WASM single-page app — embedded directly in the binary.

Enabling it

The console is behind the embed-ui Cargo feature. The Debian package and the release binaries are built with it on; if you build from source, include the feature (see Installation):

$ dx build --release --package raptor-ui
$ cargo build --release --features embed-ui

Without the feature the server runs identically but does not serve the UI routes.

Accessing it

Browse to /ui:

http://localhost:8088/ui

Log in with the same admin credentials as the Management API. The console authenticates via POST /rest/v1/login and holds a session cookie, so you don’t re-enter credentials on every request.

What it covers

The console surfaces the core read/observe workflow and common actions:

  • a dashboard (fleet counters, the actions feed, active rollouts, and the server’s configuration),
  • targets and target detail,
  • target filters and auto-assignment,
  • tags — create and edit target and distribution-set tags, tag an entity from its detail page, and filter the target and distribution lists by tag,
  • distribution sets and detail,
  • software modules and detail,
  • rollouts and rollout detail,
  • the actions feed.

Dashboard

The counter tiles — targets, in sync, pending, error, running actions — come from GET /rest/v1/system/statistics, so the server counts the whole fleet and the numbers stay correct however large it grows. The active-rollout and recent-action lists below them are feeds rather than counts and read the ordinary list endpoints. Everything refreshes on the console’s 5s polling.

The System configuration card at the foot of the dashboard shows the tenant configuration devices see (GET /rest/v1/system/configs): the polling interval, whether the confirmation flow is on, and which authentication modes are enabled. It is read-only — raptor takes its configuration from raptor.toml, and the API answers writes to these keys with 403. See Configuration to change any of them.

Rollouts

The rollouts list shows each rollout’s status and a progress bar of its targets; the detail page adds the deploy groups, each with its own bar and a legend of targets per status (finished / running / error / cancelled / scheduled / not started), refreshed by the same 5s polling as the rest of the console.

Lifecycle buttons — Start, Pause, Resume, Delete — appear for the transitions the rollout’s current status allows, so an operator can drive a rollout end to end without touching the API. Creating a rollout is still API-only; see the Rollouts guide.

Note: The console tracks the API and lags it slightly. A page for the confirmation flow is planned (tracked as an issue on GitHub). Anything not yet in the UI is always available through the Management API.

Architecture

raptor is a modular monolith: one process, one binary, with clear internal seams between the HTTP layer, the domain logic, and persistence.

Workspace

raptor/
├── Cargo.toml            # workspace
├── raptor/               # main crate (bin + lib)
│   └── src/
│       ├── main.rs           # CLI (serve, hash-password), startup, background tasks
│       ├── app.rs            # router assembly
│       ├── config.rs         # TOML + RAPTOR_* env overrides
│       ├── api/
│       │   ├── ddi/          # /{tenant}/controller/v1/... device handlers
│       │   └── mgmt/         # /rest/v1/... operator handlers
│       ├── domain/           # deployment state machine, rollouts, auto-assign
│       ├── entity/           # SeaORM entities
│       ├── fiql/             # FIQL/RSQL parser -> SeaORM Condition
│       ├── storage.rs        # content-addressed artifact store
│       └── auth/             # tower middleware for both auth zones
├── raptor-api-types/     # shared Management API DTOs (also compiles to wasm)
├── raptor-ui/            # Dioxus web console (wasm32)
└── migration/            # sea-orm-migration crate (one set, both DBs)

Stack

  • axum — HTTP routing and extractors.
  • SeaORM — one entity/migration definition targeting both SQLite and Postgres; the backend is chosen by the database_url scheme.
  • tokio — async runtime; also drives background tasks.
  • winnow — the FIQL parser.
  • sha2 / sha1 / md-5 — artifact hashing.
  • argon2 — admin password hashing.
  • Dioxus — the optional embedded web console.

Request path

  1. A request hits axum. A tower middleware enforces the right auth zone — ddi_auth for /{tenant}/controller/v1/..., mgmt_auth for /rest/v1/....
  2. The handler (api/ddi or api/mgmt) validates input and calls into the domain layer.
  3. The domain layer owns the rules — the action state machine, rollout evaluation, auto-assignment — and talks to persistence through entities.
  4. Artifact bytes bypass the database: they stream to/from the content-addressed store on disk.

Background tasks

A single tokio task runs on a fixed interval (rollout_eval_interval_secs, default 5s) and performs two jobs:

  • Rollout evaluation — advances or pauses running rollout groups based on action outcomes.
  • Auto-assignment sweep — assigns target-filter distribution sets to newly-matching targets.

Shared DTOs

The raptor-api-types crate holds the Management API request/response types. It compiles to wasm32 as well as native, so the server and the Dioxus web console share exactly the same type definitions — the JSON contract can’t drift between them. Round-trip tests assert the JSON shape matches hawkBit.

Persistence & migrations

There is one SeaORM entity set and one ordered list of migrations. They run automatically at startup against whichever database database_url points to, so upgrading raptor and migrating the schema is a single step: start the new binary.

Domain Model

The entities below are the nouns of raptor. Understanding how they relate makes the API and the update lifecycle straightforward.

SoftwareModuleType        DistributionSetType
        │                         │
        ▼                         ▼
  SoftwareModule ──< DsModule >── DistributionSet
        │                              │
        ▼                              │ assigned / installed
    Artifact                          ▼
   (blob on disk)     Target ──< Action >── (a deployment)
        ▲                │            │
        │                ▼            ▼
   content-addressed  TargetAttribute  ActionStatus
                                        (+ messages)
              Rollout ──< RolloutGroup >── RolloutTargetGroup
              TargetFilter (optional auto-assign DS)

Core entities

  • Target — a device, keyed by unique controllerId. Holds a security token, an updateStatus (unknownregisteredpendingin_sync / error), the last poll time and request address, an auto_confirm flag, and pointers to its assigned and installed distribution sets.
  • TargetAttribute — key/value pairs a device reports via DDI configData.
  • SoftwareModule — a named, versioned unit of a seeded type (os, firmware, runtime, application). Owns artifacts.
  • Artifact — a file (filename, size, sha1/md5/sha256) belonging to one module. The blob is stored once per sha256 on disk; artifact rows are references to it.
  • DistributionSet — a named, versioned bundle of modules of a seeded type (os, os_app, app). complete when it has the modules its type requires; only complete sets are assignable.
  • Action — one deployment of a DS to a target. Carries a status (the action state machine), an active flag, and a forced/soft indicator. May belong to a rollout group. Invariant: at most one active action per target.
  • ActionStatus — an append-only history row (with optional messages) written on every device feedback and every server-side transition.

Rollout entities

  • Rollout — a staged deployment of a DS to the targets matched by a FIQL filter, split into groups, with success/error thresholds.
  • RolloutGroup — one stage; has its own status and thresholds.
  • RolloutTargetGroup — the membership join: which targets are in which group (a static snapshot taken at creation).

Target filters

  • TargetFilter — a saved FIQL query with an optional attached auto-assign distribution set and action type. Drives auto-assignment.

Seeded types

Software-module and distribution-set types are read-only in this version, created by the initial migration:

  • Software-module types: os, firmware, runtime, application
  • Distribution-set types: os, os_app, app

Tenancy

There is no tenant column. raptor is single-tenant; the DDI tenant URL segment is accepted and ignored, and all generated links use DEFAULT.

Update Lifecycle

This is the heart of raptor: how an assignment becomes an installed update, and how each party’s actions move the state machine.

The happy path

operator                 server                         device
────────                 ──────                         ──────
assignedDS  ──────────▶  Action(running), target=pending
                                                 ◀──────  poll  (sees deploymentBase)
                                                 ◀──────  GET deploymentBase
                         (streams artifacts)     ◀──────  download artifacts
                                                 ◀──────  feedback: proceeding
                         ActionStatus += proceeding
                                                 ◀──────  feedback: closed/success
                         Action(finished), target=in_sync
                                                 ◀──────  poll  (no deploymentBase;
                                                                 installedBase set)
  1. Assign. POST /rest/v1/targets/{id}/assignedDS creates an Action in running (or wait_for_confirmation if the confirmation flow is on), and sets the target to pending. Any prior active action is cancelled.
  2. Poll. The device polls the DDI root and sees _links.deploymentBase.
  3. Fetch & download. The device gets deploymentBase/{actionId} and downloads the listed artifacts (with HTTP Range resume).
  4. Feedback. The device reports proceeding, then a final result. Each report appends an ActionStatus row.
  5. Complete. On closed/success the Action becomes finished, the target becomes in_sync, and installedBase reflects the deployment. On closed/failure the Action becomes error and the target becomes error.

Feedback vocabulary

Device feedback is {"status":{"execution": ..., "result":{"finished": ...}}}.

  • executionproceeding, scheduled, resumed, downloading, downloaded, canceled, rejected, closed.
  • result.finishednone, success, failure.

Only closed (with success or failure) is terminal; the others are recorded as history and leave the action active.

Confirmation flow

With the confirmation flow enabled, a new assignment lands in wait_for_confirmation and the poll offers confirmationBase instead of deploymentBase. A confirmed feedback moves it to running; the next poll then follows the happy path above.

Cancellation

operator                 server                         device
────────                 ──────                         ──────
DELETE action ────────▶  Action(canceling)
                                                 ◀──────  poll  (sees cancelAction)
                                                 ◀──────  GET cancelAction
                                                 ◀──────  cancel feedback: closed
                         Action(canceled); assigned reverts to installed

A normal cancel sets the action to canceling and offers the device a cancelAction link; the device confirms and the action becomes canceled. A forced cancel (?force=true) closes it immediately, without waiting for the device.

Auto-registration

An unknown controllerId that polls with a valid gateway token (or in anonymous mode) is created on the spot as registered. If an auto-assignment filter matches it, the DS is assigned during that same poll — so a brand-new device can receive a deploymentBase on its very first request.

Configuration Reference

raptor reads a TOML file (default raptor.toml, override with serve --config <path>). Every key can be overridden by a RAPTOR_* environment variable; nested tables use a __ separator (e.g. RAPTOR_DDI__ANONYMOUS).

Top level

KeyTypeDefaultDescription
bindsocket addr0.0.0.0:8088address the HTTP server listens on
database_urlstring(required)sqlite://… or postgres://…; selects the backend
artifact_dirpath(required)root of the content-addressed artifact store
max_artifact_sizeinteger (bytes)1073741824 (1 GiB)maximum artifact upload size
urlstring(unset)external base URL for _links; derived from the Host header when unset
rollout_eval_interval_secsinteger5how often the background evaluator / auto-assign sweep runs

[ddi] — device-facing API

KeyTypeDefaultDescription
anonymousboolfalsedisable all DDI auth (dev only)
gateway_tokenstring(unset)shared token; enables auto-registration
polling_intervalstring HH:MM:SS00:05:00poll sleep advertised to devices
confirmation_flowboolfalserequire confirmation before a deployment starts
auto_confirm_defaultboolfalsegive newly created targets autoConfirm, so confirmation_flow can’t strand confirmation-unaware clients
artifact_http_urlstring(unset)plain-HTTP base advertised in the DDI download-http links; unset means they reuse url
trusted_proxy_headerstring(unset)header to read the device address from behind a reverse proxy, e.g. x-forwarded-for; unset uses the socket peer

[mgmt] — Management API / web console

KeyTypeDefaultDescription
usernamestring(required)admin username
password_hashstring(required)argon2id hash from raptor hash-password

Example

bind = "0.0.0.0:8088"
database_url = "postgres://raptor:raptor@localhost/raptor"
artifact_dir = "/var/lib/raptor/artifacts"
max_artifact_size = 2147483648            # 2 GiB
url = "https://raptor.example.com"
rollout_eval_interval_secs = 10

[ddi]
anonymous = false
gateway_token = "shared-registration-secret"
polling_interval = "00:05:00"
confirmation_flow = true

[mgmt]
username = "admin"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."

Environment overrides

RAPTOR_BIND=127.0.0.1:9090
RAPTOR_DATABASE_URL=sqlite://raptor.db?mode=rwc
RAPTOR_DDI__ANONYMOUS=true
RAPTOR_DDI__GATEWAY_TOKEN=super-secret
RAPTOR_MGMT__PASSWORD_HASH='$argon2id$...'

Environment values take precedence over the TOML file — the recommended way to inject secrets.

Management API Reference

Operator-facing REST API under /rest/v1, authenticated with HTTP Basic (or a session cookie). All list endpoints accept offset, limit, sort=field:ASC|DESC, and q=<FIQL>, and return the hawkBit paged envelope {content, total, size}.

Base URL examples assume localhost:8088.

Handlers live under raptor/src/api/mgmt/, one module per resource family, each exposing a routes() that mod.rs merges — e.g. target endpoints in targets.rs, distribution-set/software-module/target type endpoints in types/.

Auth & session

MethodPathDescription
POST/rest/v1/loginexchange credentials for a session cookie
POST/rest/v1/logoutclear the session
GET/rest/v1/session204 if the request is authenticated, 401 if not
GET/healthliveness probe (returns ok)

Targets

MethodPathDescription
POST/rest/v1/targetscreate targets (JSON array)
GET/rest/v1/targetslist (paging/sort/FIQL)
GET/rest/v1/targets/{cid}get one
PUT/rest/v1/targets/{cid}update name/description/token/requestAttributes
DELETE/rest/v1/targets/{cid}delete
GET/rest/v1/targets/{cid}/attributesdevice-reported attributes
POST / DELETE/rest/v1/targets/{cid}/targettypeassign / unassign the target type
POST / GET/rest/v1/targets/{cid}/metadatacreate (JSON array) / list metadata

| GET / PUT / DELETE | /rest/v1/targets/{cid}/metadata/{key} | get / update / delete one entry | | POST | /rest/v1/targets/{cid}/assignedDS | assign a DS (creates an action) | | GET | /rest/v1/targets/{cid}/assignedDS | currently assigned DS (or 204) | | GET | /rest/v1/targets/{cid}/installedDS | last installed DS (or 204) | | GET | /rest/v1/targets/{cid}/actions | actions for this target | | GET | /rest/v1/targets/{cid}/actions/{aid} | one action | | PUT | /rest/v1/targets/{cid}/actions/{aid} | escalate the force type, {"forceType":"forced"} | | GET | /rest/v1/targets/{cid}/actions/{aid}/status | action status history (paging/sort) | | DELETE | /rest/v1/targets/{cid}/actions/{aid} | cancel (?force=true to force) | | GET | /rest/v1/targets/{cid}/autoConfirm | auto-confirm state | | POST | /rest/v1/targets/{cid}/autoConfirm/activate | enable auto-confirm | | POST | /rest/v1/targets/{cid}/autoConfirm/deactivate | disable auto-confirm |

A target’s requestAttributes flag controls whether its DDI poll advertises the configData link. It is set when the target is created and cleared once the device uploads its attributes; re-arm it to ask for a fresh upload:

curl -u admin:pw -X PUT localhost:8088/rest/v1/targets/device-42 \
  -H 'Content-Type: application/json' -d '{"requestAttributes": true}'

Embedded set and tag references

GET /rest/v1/targets and GET /rest/v1/targets/{cid} include the target’s installed and assigned distribution sets, and its tags, inline:

{
  "controllerId": "dev-a91f3c",
  "updateStatus": "pending",
  "installedDs": { "id": 4, "name": "gw-linux", "version": "2026.05", "type": "os_app" },
  "assignedDs":  { "id": 7, "name": "gw-linux", "version": "2026.07", "type": "os_app" },
  "tags": [{ "id": 2, "name": "linux-gw", "colour": "#4f9cf9" }]
}

A raptor extension, and additive: hawkBit exposes these only as installedDS, assignedDS and per-tag reverse lookups, so a list view could otherwise only fetch them one request per row. The fields are omitted when absent — no installed set is different from an empty one — and every other endpoint that returns a target (create, tag assignment) serialises exactly as before.

Resolved in a fixed number of queries for the whole page regardless of page size, because assigned_ds_id and installed_ds_id are columns on the target row.

The set type is the type key (os, app, os_app), not its id: it is what says whether a given device class can install that set at all.

Software modules & artifacts

MethodPathDescription
POST / GET/rest/v1/softwaremodulescreate / list
GET / PUT / DELETE/rest/v1/softwaremodules/{id}get / update / delete
POST / GET/rest/v1/softwaremodules/{id}/artifactsupload (multipart) / list
GET / DELETE/rest/v1/softwaremodules/{id}/artifacts/{aid}get / delete
GET/rest/v1/softwaremodules/{id}/artifacts/{aid}/downloaddownload
POST / GET/rest/v1/softwaremodules/{id}/metadatacreate (JSON array) / list metadata
GET / PUT / DELETE/rest/v1/softwaremodules/{id}/metadata/{key}get / update / delete one entry (targetVisible surfaces to devices)

Distribution sets

MethodPathDescription
POST / GET/rest/v1/distributionsetscreate / list
GET / PUT / DELETE/rest/v1/distributionsets/{id}get / update / delete
POST/rest/v1/distributionsets/{id}/invalidateinvalidate (stops rollouts / auto-assign, cancels actions)
POST / GET/rest/v1/distributionsets/{id}/assignedSMadd / list modules
POST / GET/rest/v1/distributionsets/{id}/metadatacreate (JSON array) / list metadata
GET / PUT / DELETE/rest/v1/distributionsets/{id}/metadata/{key}get / update / delete one entry

Actions (fleet-wide)

MethodPathDescription
GET/rest/v1/actionslist all actions (paging/sort/FIQL)
GET/rest/v1/system/configstenant configuration (read-only; file-driven)
GET / PUT / DELETE/rest/v1/system/configs/{key}one config key (writes → 403)
GET/rest/v1/system/statisticsfleet counters (targets/actions/…), optional q=

Scoping statistics to a filter

/rest/v1/system/statistics accepts an optional FIQL q=, the same query the target list and saved target filters take:

curl -u admin:pw 'localhost:8088/rest/v1/system/statistics?q=tag%3D%3Dlinux-gw'

q narrows the target counters — totalTargets and targetsByStatus — to the matching targets. It lets a caller read one saved filter’s in-sync / pending / error split in a single request rather than one request per status.

The catalogue and action counters (totalDistributionSets, totalSoftwareModules, totalActions, totalRollouts, activeActions) stay fleet-wide whether or not q is set: scoping “how many distribution sets exist” to a set of targets has no meaning, and reporting them as 0 would read as missing data. An unparseable or unknown-field query returns 400.

Types

Software-module, distribution-set and target types support full CRUD. The default os / firmware / runtime / application module types and os / os_app / app DS types are seeded, and a DS type’s mandatory module types drive whether a distribution set is complete. Deleting a type that is still in use returns 409.

MethodPathDescription
POST / GET/rest/v1/softwaremoduletypescreate / list module types
GET / PUT / DELETE/rest/v1/softwaremoduletypes/{id}get / update (description) / delete
POST / GET/rest/v1/distributionsettypescreate (with mandatory/optional module types) / list
GET / PUT / DELETE/rest/v1/distributionsettypes/{id}get / update (description) / delete
GET / POST/rest/v1/distributionsettypes/{id}/mandatorymoduletypeslist / add mandatory module type
DELETE/rest/v1/distributionsettypes/{id}/mandatorymoduletypes/{mid}remove mandatory module type
GET / POST/rest/v1/distributionsettypes/{id}/optionalmoduletypeslist / add optional module type
DELETE/rest/v1/distributionsettypes/{id}/optionalmoduletypes/{mid}remove optional module type
POST / GET/rest/v1/targettypescreate (with compatible DS types) / list
GET / PUT / DELETE/rest/v1/targettypes/{id}get / update / delete
GET / POST/rest/v1/targettypes/{id}/compatibledistributionsettypeslist / add compatible DS type
DELETE/rest/v1/targettypes/{id}/compatibledistributionsettypes/{dsid}remove compatible DS type

Tags

Target and distribution-set tags are free-form labels (name, description, colour) used for fleet organisation and as the tag== FIQL term on the target and distribution-set lists. Deleting a tag removes its assignments; the tagged targets and sets are untouched. Assignment is idempotent — re-assigning an already-tagged entity succeeds without creating a duplicate.

MethodPathDescription
POST / GET/rest/v1/targettagscreate (array body) / list
GET / PUT / DELETE/rest/v1/targettags/{id}get / update / delete
GET/rest/v1/targettags/{id}/assignedlist tagged targets (paging, sort=, q=)
GET/rest/v1/targets/{cid}/tagstags carried by one target (raptor extension)
POST / DELETE/rest/v1/targettags/{id}/assignedbulk assign / unassign, body ["dev-1","dev-2"]
POST / DELETE/rest/v1/targettags/{id}/assigned/{cid}assign / unassign one target
POST / GET/rest/v1/distributionsettagscreate (array body) / list
GET / PUT / DELETE/rest/v1/distributionsettags/{id}get / update / delete
GET/rest/v1/distributionsettags/{id}/assignedlist tagged distribution sets
GET/rest/v1/distributionsets/{id}/tagstags carried by one set (raptor extension)
POST / DELETE/rest/v1/distributionsettags/{id}/assignedbulk assign / unassign, body [1,2]
POST / DELETE/rest/v1/distributionsettags/{id}/assigned/{dsid}assign / unassign one set

Rollouts

MethodPathDescription
POST / GET/rest/v1/rolloutscreate / list
GET / DELETE/rest/v1/rollouts/{id}get / delete
POST/rest/v1/rollouts/{id}/startstart (schedules first group)
POST/rest/v1/rollouts/{id}/pausepause
POST/rest/v1/rollouts/{id}/resumeresume
GET/rest/v1/rollouts/{id}/deploygroupslist groups
GET/rest/v1/rollouts/{id}/deploygroups/{gid}one group
GET/rest/v1/rollouts/{id}/deploygroups/{gid}/targetscontrollerIds in a group

Rollout and group payloads carry totalTargetsPerStatus (notstarted, scheduled, running, error, finished, cancelled) — see the Rollouts guide.

Target filters

MethodPathDescription
POST / GET/rest/v1/targetfilterscreate / list
GET / PUT / DELETE/rest/v1/targetfilters/{id}get / update / delete
GET / POST / DELETE/rest/v1/targetfilters/{id}/autoAssignDSread / attach / detach auto-assign DS

Common status codes

CodeWhen
200 / 201success / created
204no content (e.g. no assigned DS)
400invalid FIQL or malformed body
401bad or missing credentials
404unknown entity
409duplicate key (e.g. module name+version+type)
410feedback for a non-active action

See Error Codes for the response body shape.

DDI API Reference

The device-facing API, under /{tenant}/controller/v1/{controllerId}. Requests are authenticated by target token, gateway token, or anonymous mode — see Authentication.

Configure clients with tenant DEFAULT. raptor is single-tenant: the tenant path segment is accepted and ignored, and every emitted link says DEFAULT. A device configured with anything else (Zephyr: CONFIG_HAWKBIT_TENANT) still works — it just follows hrefs that disagree with its own config, and it would break if multi-tenancy landed later. raptor logs a warning the first time it sees a non-DEFAULT poll.

Response JSON matches the hawkBit DDI v1 schemas field-for-field.

Handlers live under raptor/src/api/ddi/, one module per resource: poll root in root.rs, deployment/installed base in deployment.rs, feedback/cancel in feedback.rs, confirmation flow in confirmation.rs, and artifacts in artifacts.rs, all wired together in mod.rs.

Endpoints

MethodPath (under /{tenant}/controller/v1/{cid})Description
GET/poll root: config.polling.sleep + _links
PUT/configDatareport device attributes (merge / replace / remove)
GET/deploymentBase/{actionId}the deployment to install
POST/deploymentBase/{actionId}/feedbackdeployment progress/result
GET/confirmationBase/{actionId}pending deployment awaiting confirmation
POST/confirmationBase/{actionId}/feedbackconfirm / deny
POST/confirmationBase/activateAutoConfirmdevice enables auto-confirm
POST/confirmationBase/deactivateAutoConfirmdevice disables auto-confirm
GET/cancelAction/{actionId}cancellation to acknowledge
POST/cancelAction/{actionId}/feedbackconfirm cancellation
GET/installedBase/{actionId}last successfully installed deployment
GET/softwaremodules/{moduleId}/artifactsartifact list for a module
GET/softwaremodules/{moduleId}/artifacts/{filename}artifact download (HTTP Range)
GET/softwaremodules/{moduleId}/artifacts/{filename}.MD5SUMmd5sum-file

Poll root

{
  "config": { "polling": { "sleep": "00:05:00" } },
  "_links": {
    "configData":       { "href": ".../configData" },
    "deploymentBase":   { "href": ".../deploymentBase/7" }
  }
}

Which _links appear depends on the target’s state: deploymentBase when an action is running, confirmationBase when it’s wait_for_confirmation, cancelAction when it’s canceling, and installedBase once something has been installed.

configData appears only while raptor actually wants attributes — a freshly registered target, or one an operator has re-armed with requestAttributes (see Management API). It disappears as soon as a configData PUT arrives. This matters because some clients (the Zephyr hawkbit client among them) re-upload their entire attribute set on every poll that carries the link, which is wasted uplink on a large or cellular fleet.

deploymentBase

{
  "id": "7",
  "deployment": {
    "download": "forced",
    "update": "forced",
    "chunks": [
      { "part": "os", "version": "1.0", "name": "rootfs",
        "artifacts": [
          { "filename": "rootfs.img", "size": 12345,
            "hashes": { "sha1": "...", "md5": "...", "sha256": "..." },
            "_links": { "download-http": {"href": "..."},
                        "md5sum-http":  {"href": "..."} } }
        ],
        "metadata": [ { "key": "signature", "value": "sig-1" } ] }
    ]
  },
  "actionHistory": { "status": "RUNNING", "messages": [] }
}

download/update follow the action’s type, matching hawkBit’s calculateDownloadType/calculateUpdateType:

Action typedownloadupdate
forcedforcedforced
softattemptattempt
timeforced, before forcetimeattemptattempt
timeforced, after forcetimeforcedforced
downloadonlyforcedskip

The modes are computed per request, so a timeforced action flips on its own once the deadline passes. See Assignments & Actions for the operator side. The confirmationBase response is identical but keyed confirmation instead of deployment.

A chunk’s metadata array carries any software-module metadata marked targetVisible (see the Management API). The key is omitted entirely when a module has no visible metadata.

Feedback

{ "status": { "execution": "closed", "result": { "finished": "success" } } }
  • executionproceeding, scheduled, resumed, downloading, downloaded, canceled, rejected, closed.
  • result.finishednone, success, failure.

closed is terminal for every action type. For a downloadonly action downloaded is also terminal — that is the whole job, so the action closes with detailStatus: downloaded and the target’s installed DS is left unchanged. For all other types downloaded is recorded as progress only. Posting feedback to a non-active action returns 410 Gone.

Confirmation feedback uses a different body:

{ "confirmation": "confirmed", "details": ["…"] }   // or "denied"

configData

{ "mode": "merge", "data": { "hw": "rev2", "os": "linux" } }

modemerge (default; upsert keys), replace (drop all, then set), remove (delete the listed keys). Extra legacy fields in the body are ignored.

Artifact download & Range

The artifact download endpoint honors HTTP Range (RFC 7233) so an interrupted download resumes with a 206 Partial Content response rather than restarting.

download-http vs download

Each artifact carries up to two link families, following hawkBit’s convention: download-http / md5sum-http are the plain-HTTP URLs, download / md5sum the HTTPS ones. Clients pick one and take the scheme from the href itself — the Zephyr hawkbit client uses download-http.

raptor only advertises a genuinely different plain-HTTP URL when you have told it one is reachable, via [ddi] artifact_http_url:

url = "https://ota.example.com"
[ddi]
artifact_http_url = "http://dl.example.com:8088"
Configdownload-httpdownload
url http, no artifact_http_urlthe url (http)(absent)
url https, no artifact_http_urlthe url (https)the url (https)
url https + artifact_http_urlartifact_http_url (http)the url (https)

The middle row is why download-http can carry an https:// href: on a TLS-only deployment there is no plain-HTTP port to point at, and emitting one anyway would break every client that follows the link. Set artifact_http_url when you want device downloads to bypass TLS.

CLI Reference

The raptor binary has two subcommands.

raptor serve

Runs the server: loads config, connects to the database, applies migrations, starts the background evaluator, and serves HTTP.

$ raptor serve [--config <path>]
FlagDefaultDescription
--config <path>raptor.tomlpath to the TOML config file

Migrations are applied automatically before the listener starts. The server logs to stdout/stderr; control verbosity with the RUST_LOG environment variable, e.g. RUST_LOG=raptor=debug,tower_http=info.

$ RUST_LOG=raptor=debug raptor serve --config /etc/raptor/config.toml
raptor listening bind=0.0.0.0:8088

raptor hash-password

Reads a password from stdin and prints its argon2id hash for use in mgmt.password_hash.

$ printf 'yourpassword\n' | raptor hash-password
$argon2id$v=19$m=19456,t=2,p=1$...

Pipe from a file or a secrets manager rather than typing interactively in scripts. The output is safe to store in the config file (it’s a hash, not the password).

Version

$ raptor --version

Error Codes

raptor returns hawkBit-shaped error bodies so that existing clients — which branch on the HTTP status code and sometimes the errorCode string — behave identically against raptor.

Body shape

{
  "exceptionClass": "org.eclipse.hawkbit.repository.exception.EntityNotFoundException",
  "errorCode": "hawkbit.server.error.repo.entitiyNotFound",
  "message": "target not found"
}

The exceptionClass and errorCode strings mirror hawkBit’s for the covered cases (including hawkBit’s historical entitiy spelling), because some clients match on them. Most clients branch on the status code, which is the hard contract.

Status codes

CodeMeaningExample
400 Bad Requestinvalid FIQL or malformed bodyq=bogusField==1, incomplete DS assignment
401 Unauthorizedbad or missing credentialswrong target token, missing Basic auth
404 Not Foundunknown entitytarget / module / action doesn’t exist
409 Conflictduplicate keymodule name+version+type, duplicate filter name
410 Gonefeedback for a non-active actiondevice reports on a finished/canceled action

Notable errorCode strings

errorCodePaired status
hawkbit.server.error.repo.entitiyNotFound404
hawkbit.server.error.rest.body.notReadable400
hawkbit.server.error.unauthorized401
hawkbit.server.error.repo.entitiyAlreadyExists409
hawkbit.server.error.repo.actionNotActive410

Auth responses

401 from the Management zone includes a WWW-Authenticate: Basic header so tools prompt for credentials. The web console’s own session checks use a “quiet” variant that omits the header, to avoid triggering the browser’s native Basic-auth dialog on the single-page app.

FAQ

Is raptor a drop-in replacement for hawkBit?

For the DDI v1 device protocol, yes — devices using SWUpdate, the RAUC hawkbit-updater, or other hawkBit DDI clients work unchanged. For the Management API, raptor implements the core workflow but not every endpoint; see hawkBit Compatibility.

Which databases are supported?

SQLite and Postgres, selected by the database_url scheme. Migrations run automatically at startup. SQLite is great for small/single-node deployments; Postgres for larger fleets or when you want an external managed database.

Does raptor need RabbitMQ?

No. raptor implements only the HTTP-based DDI device path, not hawkBit’s DMF (AMQP) path, so there is no broker to run.

Can I run multiple tenants on one raptor?

No. raptor is single-tenant. It accepts and ignores the tenant segment in DDI URLs (so tenant-configured clients still work), but there is no data isolation. Run one raptor instance per fleet.

How do devices authenticate?

Per-target security tokens, a shared gateway token (which also enables auto-registration), or anonymous mode for development. See Authentication.

Where does raptor store artifacts?

In a content-addressed store on local disk under artifact_dir, laid out as <sha256[0..2]>/<sha256>. Identical content is stored once. The Debian package defaults this to /var/lib/raptor/artifacts.

Can I offload artifact storage to S3?

Not yet — storage is local disk only. S3/object-storage backends are a tracked enhancement.

How do I change the admin password?

Generate a new hash with raptor hash-password, put it in mgmt.password_hash (or the RAPTOR_MGMT__PASSWORD_HASH env var), and restart. There is a single admin user in this version.

How do I make a deployment wait for approval?

Enable the confirmation flow ([ddi] confirmation_flow = true). Assignments then wait for the device (or an operator via auto-confirm) before deploying. Note this is per-assignment confirmation, not hawkBit’s separate rollout approval workflow, which isn’t implemented.

Why is my deploymentBase returning 404?

The action is not in a state that serves a deployment. If the confirmation flow is on, the device should be using confirmationBase until the action is confirmed; deploymentBase only serves running actions. Check the action’s detailStatus via GET /rest/v1/targets/{cid}/actions/{aid}.

How do I upgrade raptor?

Install the new binary/package and restart. Schema migrations apply automatically on startup. With the Debian package, your /etc/raptor/config.toml edits are preserved (it’s a dpkg conffile).

Where do I report bugs or request features?

On the GitHub repository. The compatibility matrix lists what’s not yet implemented; most of those items have tracking issues.