e070a3fb5f
OTA update server designed for high-concurrency, low-bandwidth deployments. GORM-backed PostgreSQL, JWT auth, device management, artefact versioning, deployment rollout with gated rate-limited downloads, and a React admin panel.
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`
|