476 lines
15 KiB
YAML
476 lines
15 KiB
YAML
|
|
openapi: "3.0.3"
|
|||
|
|
info:
|
|||
|
|
title: OTA Manifest — Daemon API
|
|||
|
|
description: |
|
|||
|
|
Device-side REST API for the OTA update platform.
|
|||
|
|
Each daemon (`mcd`) calls these endpoints to register, heartbeat,
|
|||
|
|
poll for updates, download artefacts, and report progress.
|
|||
|
|
|
|||
|
|
**10 Mbps bandwidth design points:**
|
|||
|
|
- Downloads are **rate-limited** (~25 KB/s per connection) and **gated** (max 50 concurrent).
|
|||
|
|
- The download endpoint supports **HTTP Range** (`206 Partial Content`) for resumable transfers.
|
|||
|
|
- `check-update` returns a delta patch when available to minimise bytes on the wire.
|
|||
|
|
version: "1.0.0"
|
|||
|
|
contact:
|
|||
|
|
name: Quanta OTA Team
|
|||
|
|
|
|||
|
|
servers:
|
|||
|
|
- url: http://localhost:8080/api/v1
|
|||
|
|
description: Local development server
|
|||
|
|
|
|||
|
|
tags:
|
|||
|
|
- name: Device
|
|||
|
|
description: Registration, heartbeat, and update checking
|
|||
|
|
- name: Package
|
|||
|
|
description: Artefact download
|
|||
|
|
- name: Deployment
|
|||
|
|
description: Lifecycle status reporting
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Paths
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
paths:
|
|||
|
|
|
|||
|
|
# ═══ Device ═══
|
|||
|
|
|
|||
|
|
/devices/register:
|
|||
|
|
post:
|
|||
|
|
tags: [Device]
|
|||
|
|
summary: Register or refresh a device
|
|||
|
|
description: |
|
|||
|
|
Called by the daemon on startup. If the `deviceId` already exists
|
|||
|
|
this is an upsert — all fields are updated and `lastHeartbeat` is
|
|||
|
|
refreshed.
|
|||
|
|
operationId: registerDevice
|
|||
|
|
requestBody:
|
|||
|
|
required: true
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/RegisterDeviceRequest"
|
|||
|
|
example:
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
ipAddress: "192.168.1.42"
|
|||
|
|
hostname: "gateway-east-01"
|
|||
|
|
os: "Debian 12"
|
|||
|
|
currentVersion: "1.2.0"
|
|||
|
|
groupName: "east-region"
|
|||
|
|
responses:
|
|||
|
|
"200":
|
|||
|
|
description: Device registered or updated
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/Device"
|
|||
|
|
example:
|
|||
|
|
id: 1
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
ipAddress: "192.168.1.42"
|
|||
|
|
hostname: "gateway-east-01"
|
|||
|
|
os: "Debian 12"
|
|||
|
|
currentVersion: "1.2.0"
|
|||
|
|
status: "online"
|
|||
|
|
groupName: "east-region"
|
|||
|
|
lastHeartbeat: "2026-07-03T10:30:00Z"
|
|||
|
|
createdAt: "2026-07-03T10:30:00Z"
|
|||
|
|
updatedAt: "2026-07-03T10:30:00Z"
|
|||
|
|
"400":
|
|||
|
|
$ref: "#/components/responses/BadRequest"
|
|||
|
|
|
|||
|
|
/devices/heartbeat:
|
|||
|
|
post:
|
|||
|
|
tags: [Device]
|
|||
|
|
summary: Send periodic heartbeat
|
|||
|
|
description: |
|
|||
|
|
Called by the daemon every 30–60 s. Keeps the device marked `online`
|
|||
|
|
and updates the `currentVersion` so the server always knows which
|
|||
|
|
version each device is running.
|
|||
|
|
operationId: heartbeat
|
|||
|
|
requestBody:
|
|||
|
|
required: true
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/HeartbeatRequest"
|
|||
|
|
example:
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
currentVersion: "1.2.0"
|
|||
|
|
status: "online"
|
|||
|
|
responses:
|
|||
|
|
"200":
|
|||
|
|
description: Heartbeat acknowledged
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/StatusResponse"
|
|||
|
|
"400":
|
|||
|
|
$ref: "#/components/responses/BadRequest"
|
|||
|
|
|
|||
|
|
/devices/check-update:
|
|||
|
|
get:
|
|||
|
|
tags: [Device]
|
|||
|
|
summary: Check whether an update is pending for this device
|
|||
|
|
description: |
|
|||
|
|
Polled by the daemon on a configurable interval (e.g. every 5 min).
|
|||
|
|
When `hasUpdate` is `true` the daemon should proceed to download the
|
|||
|
|
referenced artefact and then POST to `/deployments/{id}/status` as the
|
|||
|
|
install progresses.
|
|||
|
|
operationId: checkUpdate
|
|||
|
|
parameters:
|
|||
|
|
- name: deviceId
|
|||
|
|
in: query
|
|||
|
|
required: true
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
description: The device's unique identifier
|
|||
|
|
example: "mcd-7a3f-001"
|
|||
|
|
responses:
|
|||
|
|
"200":
|
|||
|
|
description: Update status for the device
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/CheckUpdateResponse"
|
|||
|
|
examples:
|
|||
|
|
hasUpdate:
|
|||
|
|
summary: Update available
|
|||
|
|
value:
|
|||
|
|
hasUpdate: true
|
|||
|
|
artefactId: 5
|
|||
|
|
version: "1.3.0"
|
|||
|
|
fileSize: 4194304
|
|||
|
|
sha256Hash: "a1b2c3d4e5f6..."
|
|||
|
|
downloadUrl: "/api/v1/packages/download/5"
|
|||
|
|
noUpdate:
|
|||
|
|
summary: No update needed
|
|||
|
|
value:
|
|||
|
|
hasUpdate: false
|
|||
|
|
"400":
|
|||
|
|
$ref: "#/components/responses/BadRequest"
|
|||
|
|
|
|||
|
|
# ═══ Package ═══
|
|||
|
|
|
|||
|
|
/packages/download/{id}:
|
|||
|
|
get:
|
|||
|
|
tags: [Package]
|
|||
|
|
summary: Download an artefact (full or delta patch)
|
|||
|
|
description: |
|
|||
|
|
Serves the artefact file with **per-connection bandwidth throttling**
|
|||
|
|
(~25 KB/s). Supports **HTTP Range** requests so interrupted downloads
|
|||
|
|
can be resumed via `Range: bytes=<offset>-`.
|
|||
|
|
|
|||
|
|
**Headers returned:**
|
|||
|
|
- `Content-Disposition: attachment; filename="<name>"`
|
|||
|
|
- `X-Checksum-SHA256: <hex>` — validate the file after download
|
|||
|
|
- `Accept-Ranges: bytes`
|
|||
|
|
- `Content-Length: <bytes>`
|
|||
|
|
|
|||
|
|
**Query parameters** are optional but recommended — they let the
|
|||
|
|
server track each device's download progress.
|
|||
|
|
operationId: downloadPackage
|
|||
|
|
parameters:
|
|||
|
|
- name: id
|
|||
|
|
in: path
|
|||
|
|
required: true
|
|||
|
|
schema:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
description: Artefact ID (from `check-update` response)
|
|||
|
|
example: 5
|
|||
|
|
- name: deviceId
|
|||
|
|
in: query
|
|||
|
|
required: false
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
description: Device identifier for progress tracking
|
|||
|
|
example: "mcd-7a3f-001"
|
|||
|
|
- name: deploymentId
|
|||
|
|
in: query
|
|||
|
|
required: false
|
|||
|
|
schema:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
description: Deployment ID for progress tracking
|
|||
|
|
example: 3
|
|||
|
|
responses:
|
|||
|
|
"200":
|
|||
|
|
description: Full artefact file
|
|||
|
|
headers:
|
|||
|
|
Content-Disposition:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
description: Attachment filename
|
|||
|
|
X-Checksum-SHA256:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
description: SHA-256 hex digest of the file
|
|||
|
|
Accept-Ranges:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
example: bytes
|
|||
|
|
content:
|
|||
|
|
application/octet-stream:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
format: binary
|
|||
|
|
"206":
|
|||
|
|
description: Partial content (resumed download)
|
|||
|
|
headers:
|
|||
|
|
Content-Range:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
example: "bytes 1048576-4194303/4194304"
|
|||
|
|
content:
|
|||
|
|
application/octet-stream:
|
|||
|
|
schema:
|
|||
|
|
type: string
|
|||
|
|
format: binary
|
|||
|
|
"404":
|
|||
|
|
$ref: "#/components/responses/NotFound"
|
|||
|
|
"503":
|
|||
|
|
description: Server at download capacity — retry later
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|||
|
|
example:
|
|||
|
|
error: "Server at capacity. Please retry later."
|
|||
|
|
|
|||
|
|
# ═══ Deployment ═══
|
|||
|
|
|
|||
|
|
/deployments/{id}/status:
|
|||
|
|
post:
|
|||
|
|
tags: [Deployment]
|
|||
|
|
summary: Report deployment status for a single device
|
|||
|
|
description: |
|
|||
|
|
Called by the daemon at each lifecycle transition:
|
|||
|
|
|
|||
|
|
1. `downloading` — started the download
|
|||
|
|
2. `downloaded` — download complete (may be omitted for small files)
|
|||
|
|
3. `verifying` — checking SHA-256 hash
|
|||
|
|
4. `installing` — applying the update
|
|||
|
|
5. `completed` — update applied, device running new version
|
|||
|
|
6. `failed` — unrecoverable error (set `error` field)
|
|||
|
|
|
|||
|
|
The server tracks these transitions per-device so operators can
|
|||
|
|
monitor rollout progress.
|
|||
|
|
operationId: updateDeploymentStatus
|
|||
|
|
parameters:
|
|||
|
|
- name: id
|
|||
|
|
in: path
|
|||
|
|
required: true
|
|||
|
|
schema:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
description: Deployment ID
|
|||
|
|
example: 3
|
|||
|
|
requestBody:
|
|||
|
|
required: true
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/DeploymentStatusRequest"
|
|||
|
|
examples:
|
|||
|
|
downloading:
|
|||
|
|
value:
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
status: "downloading"
|
|||
|
|
completed:
|
|||
|
|
value:
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
status: "completed"
|
|||
|
|
failed:
|
|||
|
|
value:
|
|||
|
|
deviceId: "mcd-7a3f-001"
|
|||
|
|
status: "failed"
|
|||
|
|
error: "SHA256 mismatch: expected a1b2.. got x9y0.."
|
|||
|
|
responses:
|
|||
|
|
"200":
|
|||
|
|
description: Status recorded
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/StatusResponse"
|
|||
|
|
"400":
|
|||
|
|
$ref: "#/components/responses/BadRequest"
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Component Schemas
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
components:
|
|||
|
|
|
|||
|
|
schemas:
|
|||
|
|
|
|||
|
|
# -- Requests --
|
|||
|
|
|
|||
|
|
RegisterDeviceRequest:
|
|||
|
|
type: object
|
|||
|
|
required: [deviceId]
|
|||
|
|
properties:
|
|||
|
|
deviceId:
|
|||
|
|
type: string
|
|||
|
|
maxLength: 256
|
|||
|
|
description: Unique device identifier (set by daemon config)
|
|||
|
|
ipAddress:
|
|||
|
|
type: string
|
|||
|
|
description: Device IP at registration time
|
|||
|
|
hostname:
|
|||
|
|
type: string
|
|||
|
|
description: OS hostname
|
|||
|
|
os:
|
|||
|
|
type: string
|
|||
|
|
description: Operating system name and version
|
|||
|
|
currentVersion:
|
|||
|
|
type: string
|
|||
|
|
description: Currently installed client version
|
|||
|
|
groupName:
|
|||
|
|
type: string
|
|||
|
|
description: Logical group for targeted rollouts (defaults to "default")
|
|||
|
|
|
|||
|
|
HeartbeatRequest:
|
|||
|
|
type: object
|
|||
|
|
required: [deviceId]
|
|||
|
|
properties:
|
|||
|
|
deviceId:
|
|||
|
|
type: string
|
|||
|
|
maxLength: 256
|
|||
|
|
currentVersion:
|
|||
|
|
type: string
|
|||
|
|
description: Currently running version
|
|||
|
|
status:
|
|||
|
|
type: string
|
|||
|
|
enum: [online, offline, upgrading]
|
|||
|
|
default: online
|
|||
|
|
|
|||
|
|
DeploymentStatusRequest:
|
|||
|
|
type: object
|
|||
|
|
required: [deviceId, status]
|
|||
|
|
properties:
|
|||
|
|
deviceId:
|
|||
|
|
type: string
|
|||
|
|
description: Device identifier
|
|||
|
|
status:
|
|||
|
|
type: string
|
|||
|
|
enum:
|
|||
|
|
- downloading
|
|||
|
|
- downloaded
|
|||
|
|
- verifying
|
|||
|
|
- installing
|
|||
|
|
- completed
|
|||
|
|
- failed
|
|||
|
|
error:
|
|||
|
|
type: string
|
|||
|
|
description: Error description (only meaningful when status = "failed")
|
|||
|
|
|
|||
|
|
# -- Responses --
|
|||
|
|
|
|||
|
|
Device:
|
|||
|
|
type: object
|
|||
|
|
properties:
|
|||
|
|
id:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
deviceId:
|
|||
|
|
type: string
|
|||
|
|
ipAddress:
|
|||
|
|
type: string
|
|||
|
|
hostname:
|
|||
|
|
type: string
|
|||
|
|
os:
|
|||
|
|
type: string
|
|||
|
|
currentVersion:
|
|||
|
|
type: string
|
|||
|
|
status:
|
|||
|
|
type: string
|
|||
|
|
enum: [online, offline, upgrading]
|
|||
|
|
groupName:
|
|||
|
|
type: string
|
|||
|
|
lastHeartbeat:
|
|||
|
|
type: string
|
|||
|
|
format: date-time
|
|||
|
|
createdAt:
|
|||
|
|
type: string
|
|||
|
|
format: date-time
|
|||
|
|
updatedAt:
|
|||
|
|
type: string
|
|||
|
|
format: date-time
|
|||
|
|
|
|||
|
|
CheckUpdateResponse:
|
|||
|
|
type: object
|
|||
|
|
properties:
|
|||
|
|
hasUpdate:
|
|||
|
|
type: boolean
|
|||
|
|
description: Whether an update is pending
|
|||
|
|
artefactId:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
description: Artefact ID to pass to the download endpoint
|
|||
|
|
version:
|
|||
|
|
type: string
|
|||
|
|
description: Target version string
|
|||
|
|
fileSize:
|
|||
|
|
type: integer
|
|||
|
|
format: int64
|
|||
|
|
description: Total file size in bytes
|
|||
|
|
sha256Hash:
|
|||
|
|
type: string
|
|||
|
|
description: Expected SHA-256 hex digest (validate after download)
|
|||
|
|
downloadUrl:
|
|||
|
|
type: string
|
|||
|
|
description: Download path relative to the server
|
|||
|
|
|
|||
|
|
StatusResponse:
|
|||
|
|
type: object
|
|||
|
|
properties:
|
|||
|
|
status:
|
|||
|
|
type: string
|
|||
|
|
example: "acknowledged"
|
|||
|
|
|
|||
|
|
ErrorResponse:
|
|||
|
|
type: object
|
|||
|
|
properties:
|
|||
|
|
error:
|
|||
|
|
type: string
|
|||
|
|
description: Human-readable error message
|
|||
|
|
|
|||
|
|
# -- Reusable responses --
|
|||
|
|
responses:
|
|||
|
|
BadRequest:
|
|||
|
|
description: Invalid request body or parameters
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|||
|
|
example:
|
|||
|
|
error: "deviceId is required"
|
|||
|
|
NotFound:
|
|||
|
|
description: Resource not found
|
|||
|
|
content:
|
|||
|
|
application/json:
|
|||
|
|
schema:
|
|||
|
|
$ref: "#/components/schemas/ErrorResponse"
|
|||
|
|
example:
|
|||
|
|
error: "Artefact not found"
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Daemon update flow (non-normative)
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
x-daemon-flow:
|
|||
|
|
polling:
|
|||
|
|
interval: "5m (configurable)"
|
|||
|
|
description: |
|
|||
|
|
1. **Startup** → `POST /devices/register`
|
|||
|
|
2. **Every N s** → `POST /devices/heartbeat`
|
|||
|
|
3. **Every 5 min** → `GET /devices/check-update?deviceId=xxx`
|
|||
|
|
4. **If hasUpdate==true** →
|
|||
|
|
a. `POST /deployments/{id}/status` → `downloading`
|
|||
|
|
b. `GET /packages/download/{artefactId}?deviceId=xxx&deploymentId={id}`
|
|||
|
|
- Use `Range: bytes=<offset>-` to resume interrupted transfers
|
|||
|
|
c. Verify SHA-256 hash against `sha256Hash` from step 3
|
|||
|
|
d. `POST /deployments/{id}/status` → `verifying`
|
|||
|
|
e. `POST /deployments/{id}/status` → `installing`
|
|||
|
|
f. Restart the Java process, confirm health
|
|||
|
|
g. `POST /deployments/{id}/status` → `completed`
|
|||
|
|
h. `POST /devices/heartbeat` with the new `currentVersion`
|
|||
|
|
- **On any failure** → `POST /deployments/{id}/status` → `failed`
|