Управление парком устройств¶
Подробный справочник по удалённому управлению edge-устройствами с центрального локального сервера (далее — ЦЛС): протокол команд, WebSocket-канал, REST API, RBAC, прошивки, аудит, модель безопасности.
Аудитория: DevOps, инженеры сопровождения, системные интеграторы, авторы консолей управления. Связанные документы: ARCHITECTURE.ru.md, API_REFERENCE.ru.md, OPERATIONS.ru.md.
Содержание¶
- Архитектура управления
- Аутентификация и выдача токенов
- Регистрация (enrollment) устройства
- Каталог команд
- Отправка команды (REST)
- Массовые команды по группам
- История команд
- Стриминг логов
- Управление конфигурацией (push_config)
- Прошивки и OTA
- Группы устройств
- Журнал аудита
- Роли и разрешения (RBAC)
- Модель безопасности
- WS-протокол консоли
- Конфигурация ЦЛС для управления
- Наблюдаемость и мониторинг
- Известные ограничения
- Диагностика и ответы на частые проблемы
1. Архитектура управления¶
┌────────────────────┐ ┌────────────────────────────────┐
│ OKTO Cloud │ │ Центральная консоль (React) │
│ (app.okto.ru) │ │ │
└─────────▲──────────┘ └───────────────┬────────────────┘
│ HTTPS (агрегация) │ REST+JWT, WS /ws/dashboard
│ │
┌─────────┴────────────────────────────────────────────┴────────────────┐
│ Центральный локальный сервер (Ktor + PostgreSQL) │
│ │
│ ┌──────────────────────────┐ ┌────────────────────────────────┐ │
│ │ DeviceConnectionRegistry │ │ CommandDispatchService │ │
│ │ (in-memory WS-сессии) │◀──│ dispatch()/dispatchToGroup() │ │
│ └──────────────────────────┘ │ eventBus SharedFlow │ │
│ ▲ └────────────────────────────────┘ │
│ │ │
│ ┌──────────────┴───────────┐ ┌────────────────────────────────┐ │
│ │ DeviceCommandRepository │ │ FirmwareService │ │
│ │ INSERT/UPDATE команды │ │ upload → sha256 → deploy │ │
│ └──────────────────────────┘ └────────────────────────────────┘ │
└─────────▲─────────────────────▲────────────────────────────────▲──────┘
│ WS /ws/device │ GET /firmware/.../artifact │
│ (команды/события) │ (SHA-256 проверка на edge) │
┌─────────┴─────────────────────┴────────────────────────────────┴───┐
│ Edge Service (Kotlin/Ktor) │
│ │
│ ServerConnectionService ──▶ CommandHandlerService │
│ (reconnect, heartbeat) (RestartService, Reboot, OTA, │
│ PullLogs, PushConfig, ExecShell) │
└────────────────────────────────────────────────────────────────────┘
Ключевые идеи:
- Единая WS-сессия. Каждое устройство держит ровно один открытый socket. Сервер отправляет команды, устройство — результаты и телеметрию.
- Персистентная история. Все команды и деплои прошивок записываются в БД. Перезапуск сервера не теряет незавершённые команды (они переводятся в статус
TIMEOUTпосле истечения). - Event bus.
CommandDispatchServiceпубликует входящий поток вeventBus: MutableSharedFlow<…>./ws/dashboardподписывается с фильтрами.
2. Аутентификация и выдача токенов¶
Два типа JWT, оба HS256-подписанные секретом auth.jwtSecret:
| Токен | Выдаётся через | sub |
scope |
Срок жизни |
|---|---|---|---|---|
| Пользовательский | POST /api/v1/auth/login |
<userId> |
user |
auth.tokenExpirationMs (24 ч дефолт) |
| Устройственный | POST /api/v1/devices/{id}/token с X-Enrollment-Key |
<deviceId> |
device |
~1 год |
Пользовательские токены дополнительно содержат claim role ∈ {ADMIN, MANAGER, OPERATOR, VIEWER}.
Каналы WS:
/ws/dashboard?token=<userJwt>— принимает только пользовательские JWT./ws/device?token=<deviceJwt>— принимает только устройственные JWT.
Проверка JWT производится плагином Authentication { jwt("user") { verifier(...) } } в Application.configurePlugins(). В WS-эндпоинтах проверка ручная — verifyUserToken() / verifyDeviceToken() в ServerManagementRoutes.kt.
Пример логина:
POST /api/v1/auth/login HTTP/1.1
Content-Type: application/json
{"username":"admin","password":"admin123"}
Ответ:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs…",
"user": {
"id": "ea44d2ea-…",
"username": "admin",
"role": "ADMIN"
}
}
}
Учётка admin/admin123 создаётся при первом запуске. Обязательно смените пароль или удалите её в продакшне.
3. Регистрация (enrollment) устройства¶
Каждое edge-устройство при первом старте обращается к ЦЛС за своим device JWT:
POST /api/v1/devices/{deviceId}/token HTTP/1.1
Host: factory.okto.ru
X-Enrollment-Key: <shared-secret>
name=edge-01&companyId=acme&productionLineId=line-1&version=1.2.3
Логика сервера (Application.kt):
- Заголовок
X-Enrollment-Keyсравнивается сauth.deviceEnrollmentKeyв constant-time режиме. При несовпадении →401 INVALID_ENROLLMENT_KEY. - Если устройство уже зарегистрировано — сразу выпускается новый device JWT (для ротации).
- Если устройства нет и
auth.allowAutoEnrollment=true— сервер делаетINSERT INTO devices(identifier, name, company_id, production_line_id, version, status=OFFLINE)и возвращает токен. - Если
allowAutoEnrollment=false— ответ403 DEVICE_NOT_REGISTERED. Устройство должно быть предварительно создано черезPOST /api/v1/devicesадминистратором.
Ответ 200:
Настройка симметричная¶
- ЦЛС:
auth.deviceEnrollmentKey+auth.allowAutoEnrollment. - Edge:
factoryServer.enrollmentKey+ опциональныеdeviceName,companyId,productionLineId.
Ротация токена устройства¶
Вызовите тот же endpoint повторно с тем же enrollment key. Старый JWT остаётся валидным до своего exp. Если нужно немедленно инвалидировать — прокрутите auth.jwtSecret (перевыпустит все токены).
4. Каталог команд¶
Все команды определены в common/api/DeviceControl.kt как sealed-иерархия DeviceCommand. Каждая имеет уникальный @SerialName.
type |
Назначение | Опасность | Роль мин. |
|---|---|---|---|
force_sync |
Немедленно обработать локальную offline-очередь (OfflineQueueService.triggerImmediate()) |
низкая | OPERATOR |
clear_queue |
Удалить PENDING (опц. DONE) строки очереди | высокая | MANAGER |
pull_logs |
Прислать последние N строк лога как LogLineEvent (поддерживает фильтр level) |
низкая | OPERATOR |
restart_service |
Gracefully завершить edge-процесс (supervisor перезапустит) | средняя | MANAGER |
reboot_os |
sudo systemctl reboot на устройстве |
высокая | ADMIN |
shutdown_os |
sudo systemctl poweroff |
высокая | ADMIN |
push_config |
Мерж JSON-патча в InMemoryDeviceConfigStore |
низкая | MANAGER |
update_firmware |
Скачать артефакт, проверить SHA-256, stage JAR, триггернуть swap-service | средняя | MANAGER |
exec_shell |
Выполнить шаблон из allow-листа ShellTemplates (сетевой пинг, diskfree и т.п.) |
средняя | MANAGER |
enable_device |
Снять флаг disabled, возобновить производство |
низкая | OPERATOR |
disable_device |
Установить disabled=true с обязательным полем reason |
высокая | ADMIN |
Схемы payload (kotlinx.serialization)¶
@Serializable @SerialName("force_sync") data class ForceSyncCmd(override val id: String) : DeviceCommand
@Serializable @SerialName("pull_logs") data class PullLogsCmd(
override val id: String,
val tailLines: Int = 500,
val level: String? = null // "DEBUG" | "INFO" | "WARN" | "ERROR"
) : DeviceCommand
@Serializable @SerialName("push_config") data class PushConfigCmd(
override val id: String,
val patchJson: String // произвольный JSON, мержится в device config
) : DeviceCommand
@Serializable @SerialName("update_firmware") data class UpdateFirmwareCmd(
override val id: String,
val releaseId: String,
val url: String, // /api/v1/firmware/releases/{id}/artifact
val sha256: String,
val version: String
) : DeviceCommand
@Serializable @SerialName("exec_shell") data class ExecShellCmd(
override val id: String,
val templateId: String, // "network-diag" | "disk-usage" | …
val args: Map<String, String> = emptyMap()
) : DeviceCommand
@Serializable @SerialName("disable_device") data class DisableDeviceCmd(
override val id: String,
val reason: String
) : DeviceCommand
Расширение каталога¶
Чтобы добавить новую команду:
- В
common/api/DeviceControl.ktдобавьте@Serializable @SerialName("my_cmd") data class MyCmd(...)вsealed interface DeviceCommand. - В
edge/service/CommandHandlerService.ktдобавьте ветку вhandle()и реализуйте хэндлер. - При необходимости — new REST endpoint в
ServerManagementRoutes.ktдля удобной отправки. - Добавьте локализацию в
management-dashboard/src/i18n/locales/*.json(keydevice.commands.my_cmd.*). - Обновите таблицу Каталог команд и тесты.
5. Отправка команды (REST)¶
POST /api/v1/devices/{id}/commands HTTP/1.1
Authorization: Bearer <userJwt>
Content-Type: application/json
{
"command": { "type": "force_sync", "id": "cmd-550e8400-e29b-41d4-a716" },
"timeoutMs": 15000
}
Поле id внутри command — это ваш commandId (UUID). Если не указать — сервер сгенерирует.
Ответ при онлайн-устройстве¶
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": {
"commandId": "cmd-550e8400-e29b-41d4-a716",
"success": true,
"output": "Force-sync completed. In-progress: 7",
"error": null,
"data": { "durationMs": 412 }
}
}
Ответ при офлайн-устройстве¶
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": {
"commandId": "cmd-…",
"success": false,
"error": "Device offline",
"output": null
}
}
Команда при этом не записывается как DISPATCHED — статус сразу FAILED.
Ответ при таймауте¶
Если устройство не прислало CommandResult за timeoutMs:
{
"success": true,
"data": {
"commandId": "cmd-…",
"success": false,
"error": "Command timed out",
"output": null
}
}
БД-статус: TIMEOUT.
Коды ошибок REST-слоя¶
| HTTP | error.code |
Причина |
|---|---|---|
| 401 | UNAUTHORIZED |
Отсутствует или невалидный JWT |
| 403 | FORBIDDEN |
Роль не позволяет эту команду |
| 404 | DEVICE_NOT_FOUND |
Устройство с таким id не зарегистрировано |
| 400 | INVALID_PAYLOAD |
Не удалось десериализовать command |
| 409 | DEVICE_DISABLED |
Попытка отправить non-enable команду на OFF |
6. Массовые команды по группам¶
POST /api/v1/device-groups/{groupId}/commands
Authorization: Bearer <userJwt>
Content-Type: application/json
{
"command": { "type": "force_sync", "id": "cmd-…" },
"timeoutMs": 30000
}
Сервер разветвляет отправку: для каждого device-id из device_group_members генерируется свежий commandId (сохраняется корреляция на groupDispatchId). Ответ:
{
"success": true,
"data": {
"groupDispatchId": "gd-…",
"results": {
"edge-01": { "commandId": "cmd-…-a", "success": true, "output": "ok" },
"edge-02": { "commandId": "cmd-…-b", "success": true, "output": "ok" },
"edge-03": { "commandId": "cmd-…-c", "success": false, "error": "Device offline" }
}
}
}
Ограничения:
- Отправка выполняется параллельно (coroutine per device) с общим
timeoutMs. - Максимум 500 устройств в одной группе (конфиг
server.management.maxGroupSize).
7. История команд¶
Список команд устройства¶
GET /api/v1/devices/{id}/commands?limit=50&offset=0&status=FAILED,TIMEOUT
Authorization: Bearer <userJwt>
Фильтры: status, type, createdBy, since=2026-04-01T00:00:00Z, until=….
Ответ:
{
"success": true,
"data": [
{
"id": "cmd-…",
"deviceId": "edge-01",
"type": "force_sync",
"status": "SUCCESS",
"createdBy": "ea44d2ea-…",
"createdAt": "2026-04-17T22:15:43.821Z",
"dispatchedAt": "2026-04-17T22:15:43.930Z",
"completedAt": "2026-04-17T22:15:44.342Z",
"timeoutMs": 15000,
"resultJson": { "success": true, "output": "…" },
"errorMessage": null
}
],
"meta": { "total": 1, "limit": 50, "offset": 0 }
}
Одна команда¶
8. Стриминг логов¶
Pull¶
Команда pull_logs возвращает «хвост» лог-файла edge-устройства как серию LogLineEvent. Каждое событие фиксируется в device_logs:
Просмотр из консоли¶
Live-tail¶
Подпишитесь на /ws/dashboard с фильтром eventTypes: ["log_line"] и deviceIds: ["edge-01"]. Каждая новая строка приходит как:
{
"type": "log_line",
"deviceId": "edge-01",
"ts": "2026-04-17T22:15:44.342Z",
"level": "WARN",
"logger": "r.o.e.s.OfflineQueueService",
"line": "Factory server returned 503, will retry in 2s",
"commandId": null
}
Ротация и хранение¶
Таблица device_logs не ротируется автоматически. Рекомендации:
- Ежедневный cron:
DELETE FROM device_logs WHERE ts < NOW() - INTERVAL '30 days'; - Партицирование по неделе/месяцу (для больших парков).
- Долгосрочный архив — выгрузка в S3/Loki через отдельный агент.
9. Управление конфигурацией (push_config)¶
REST¶
GET /api/v1/devices/{id}/config — получить текущую желаемую конфигурацию
PUT /api/v1/devices/{id}/config — установить JSON-патч
Тело PUT:
{
"configJson": "{\"scanner\":{\"timeoutMs\":5000},\"logging\":{\"level\":\"DEBUG\"}}",
"version": 3 // optimistic locking
}
Сервер сохраняет в device_configs. Если устройство онлайн и флаг autoDispatchConfig=true — немедленно отправляет PushConfigCmd.
На стороне edge¶
CommandHandlerService.handlePushConfig():
- Парсит
patchJson→Map<String, Any>. - Мержит в
InMemoryDeviceConfigStore.patch. - Возвращает
CommandResult { success=true, output="Config v${version} applied" }.
Ограничение¶
Изменения не переживают рестарт (in-memory). Чтобы применить durable-конфиг, отправьте restart_service — edge перечитает /etc/okto/application.yaml. Для полноценного hot-reload нужно реализовать PersistentDeviceConfigStore (см. ARCHITECTURE.ru.md §16).
10. Прошивки и OTA¶
10.1 Загрузка релиза¶
POST /api/v1/firmware/releases?version=1.2.3&channel=stable¬es=Hotfix&filename=edge-service.jar
Authorization: Bearer <userJwt>
Content-Type: application/octet-stream
<binary artifact>
Обработка (FirmwareService.storeRelease):
- Stream в
data/firmware/<safeVersion>-<filename>. - Считается SHA-256 на лету.
INSERT INTO firmware_releases (id, version, channel, artifact_url, sha256, size_bytes, created_by, created_at).- Возвращается
FirmwareRelease(см. API_REFERENCE.ru.md).
10.2 Деплой¶
POST /api/v1/firmware/deployments
{
"releaseId": "73e395e6-…",
"deviceIds": ["edge-01", "edge-02"]
}
Альтернативно: "groupId": "grp-eu" для деплоя по группе.
Сервер:
- Для каждого targeted device создаёт
firmware_deployments(status=PENDING). - Отправляет
UpdateFirmwareCmd { releaseId, url, sha256, version }. - При получении
CommandResult.success=true→status=SUCCESS; иначеFAILED.
Ответ синхронно возвращает Map<deviceId, CommandResult>.
10.3 Поведение устройства¶
UpdateFirmwareExecutor:
- Скачивает
url(GET с Authorization: Bearer \<deviceJwt>) в<okto.firmware.staging.dir>/edge-service-<version>.jar.part. - Проверяет SHA-256; несовпадение →
CommandResult.success=false, error="sha256 mismatch". - Переименовывает в
.jar. - Запускает
sudo systemctl start okto-edge-update(one-shot unit, запускает скрипт/usr/local/bin/okto-edge-swap-firmware). - Скрипт:
- бэкапит старый
edge-service.jarвedge-service.jar.bak, - перемещает staged JAR на место,
- вызывает
systemctl restart okto-edge. - После рестарта новый процесс подключается через enrollment (старый token) и сообщает новую
firmwareVersionвDeviceHelloMessage. ЦЛС апдейтитdevices.firmware_version.
10.4 Откат¶
Быстрый откат — отправить UpdateFirmwareCmd с предыдущей версией (она сохранена в .bak, но swap-script перезапишет её только если SHA match). Проще — загрузите предыдущий JAR как новый релиз и деплойте его.
10.5 Броский релиз (signatures)¶
Поле signatureBase64 в FirmwareRelease зарезервировано под Ed25519-подпись. В дефолтной поставке проверка выключена. Для продакшна:
- Сгенерируйте ключевую пару
ssh-keygen -t ed25519 -f okto-release. - Подпишите JAR:
openssl pkeyutl -sign -inkey okto-release -rawin -in edge-service-1.2.3.jar | base64. - Передайте signature при uploading (query param
signatureBase64=…). - Встройте публичный ключ в ресурсы edge-JAR и включите проверку в
UpdateFirmwareExecutor(методverifyEd25519(...)).
11. Группы устройств¶
Логические группы (теги) для массовых операций. Таблицы device_groups, device_group_members.
POST /api/v1/device-groups — создать { id?, name, description? }
GET /api/v1/device-groups — список с count участников
GET /api/v1/device-groups/{id} — детали + участники
PUT /api/v1/device-groups/{id} — переименовать / описание
DELETE /api/v1/device-groups/{id} — удалить (участники открепляются)
POST /api/v1/device-groups/{id}/members — { deviceIds: [...] } добавить
DELETE /api/v1/device-groups/{id}/members — { deviceIds: [...] } удалить
POST /api/v1/device-groups/{id}/commands — bulk dispatch (см. §6)
POST /api/v1/firmware/deployments — deploy с { groupId } вместо deviceIds
Стратегия: группы типично используются по географии (RU-eu, RU-sib), по роли (primary, backup), по статусу rollout (canary, stable). Устройство может состоять в нескольких группах одновременно.
12. Журнал аудита¶
Все привилегированные действия пишутся в audit_log:
| Поле | Значение |
|---|---|
id |
autoincrement |
actor_user_id |
id пользователя, инициировавшего действие (system для бэкофиса) |
action |
LOGIN_SUCCESS, LOGIN_FAILED, DEVICE_COMMAND_DISPATCHED, FIRMWARE_UPLOADED, FIRMWARE_DEPLOYED, USER_CREATED, USER_ROLE_CHANGED, CONFIG_UPDATED, … |
entity_type |
device, firmware_release, user, group, … |
entity_id |
id затронутой сущности |
ip |
IP клиента (из X-Forwarded-For / remoteAddress) |
user_agent |
заголовок User-Agent |
meta_json |
произвольные метаданные (например, { "reason": "end of shift" }) |
ts |
timestamp |
Запрос:
GET /api/v1/audit-log?userId=…&entityId=…&action=DEVICE_COMMAND_DISPATCHED&since=2026-04-01T00:00:00Z&limit=200
Пагинация — limit (до 500) + offset. Для экспорта сделайте цикл чтения / добавьте отдельный endpoint CSV-dump.
13. Роли и разрешения (RBAC)¶
Роли встроены в enum UserRole и проверяются в хэндлерах:
| Роль | Что можно |
|---|---|
| ADMIN | Всё: CRUD пользователей и терминалов, все команды включая reboot/shutdown/disable, upload и деплой прошивок, глобальная конфигурация, удаление данных. |
| MANAGER | Всё, что OPERATOR + clear_queue, restart_service, push_config, update_firmware, создание/редактирование группы, upload прошивок, просмотр пользователей. |
| OPERATOR | Просмотр сводки и парка, отправка force_sync, pull_logs, enable_device. |
| VIEWER | Только чтение: устройства, команды, аудит, прошивки. Отправка команд запрещена. |
Enforcement:
Ktor Authentication("user")проверяет JWT.- В каждом хэндлере:
val actor = call.authenticatedUser(authService) ?: return call.respondUnauthorized(). - Затем
actor.requireRole(UserRole.MANAGER)— бросает 403 если роль ниже.
См. AuthRoutes.kt и ServerManagementRoutes.kt для конкретных проверок.
Добавление новой роли¶
- В
common/domain/Auth.ktдобавьте значение в enumUserRole. - Обновите
requireRole/ таблицу разрешений. - В
management-dashboardдобавьте локализацию и селектор в диалоге создания пользователя.
14. Модель безопасности¶
Принципы¶
- Минимальные привилегии. Edge работает из-под пользователя
okto. Права на sudo — только на 4 команды. - Whitelist для
exec_shell. Произвольный shell запрещён. Список шаблонов —ShellTemplates.ktна edge. - Integrity прошивок. SHA-256 обязательна, Ed25519 — опциональная.
- TLS везде в продакшне. Reverse-proxy (Caddy / nginx / Traefik) терминирует TLS; ЦЛС слушает на HTTP, но только по loopback.
- JWT с коротким TTL для пользователей (24 ч).
Угрозы и митигации¶
| Угроза | Митигация |
|---|---|
| Кража device JWT из query-string (access log reverse-proxy) | Стрипать ?token=… в конфиге proxy, либо first-message auth |
| Злонамеренная прошивка | SHA-256 + Ed25519 (включите в проде), audit_log всех upload |
| Brute-force login | Rate-limit POST /auth/login (добавить fail2ban / Ktor plugin) |
Leak auth.jwtSecret |
Поворот секрета инвалидирует все токены; хранить в секрет-сторе |
| Supply-chain в зависимостях | ./gradlew dependencyCheck, Renovate-bot, pinned versions |
| Перехват WS | TLS (wss://) обязательно в проде |
| SQL-injection | Exposed-DSL параметризует запросы автоматически |
| XSS в консоли | React auto-escape; dangerouslySetInnerHTML — запрещено |
Секреты в конфиге¶
auth.jwtSecret— выньте из YAML в env varOKTO_AUTH_JWT_SECRET.cloudSync.authToken— аналогично.auth.deviceEnrollmentKey— аналогично.- Hoplite поддерживает
${ENV_VAR}подстановку вapplication.yaml.
15. WS-протокол консоли¶
Подключение:
После open сервер ожидает subscription-сообщение в течение 5 секунд, иначе закрывает соединение. Минимально:
Пустые массивы = получать всё.
Типы входящих событий¶
{"type":"status","deviceId":"edge-01","status":"ONLINE","ts":"2026-04-17T22:15:43Z","metrics":{"cpuUsage":42.5,"memoryUsage":61.2,"offlineQueueDepth":2}}
{"type":"scan","deviceId":"edge-01","code":"0104650001234567211234pl","valid":true,"ts":"…"}
{"type":"print","deviceId":"edge-01","code":"0104650001234567211234pl","printer":"videojet-1","ts":"…"}
{"type":"alert","deviceId":"edge-01","severity":"WARN","message":"Printer offline","ts":"…"}
{"type":"cmd_result","deviceId":"edge-01","commandId":"cmd-…","success":true,"output":"…","ts":"…"}
{"type":"cmd_progress","deviceId":"edge-01","commandId":"cmd-…","percent":42,"message":"Downloading…","ts":"…"}
{"type":"log_line","deviceId":"edge-01","ts":"…","level":"INFO","logger":"…","line":"…","commandId":null}
Фильтрация¶
Отправьте новую subscribe-команду в любой момент — сервер заменит фильтры:
{"type":"subscribe","deviceIds":["edge-01","edge-02"],"eventTypes":["status","cmd_result","log_line"]}
Отписка¶
Сервер перестанет слать события, но соединение не закроется.
Heartbeat¶
Сервер раз в 25 с шлёт PING (WS-фрейм). Клиент должен ответить PONG. При отсутствии pong 2 раза подряд — сервер закрывает socket.
16. Конфигурация ЦЛС для управления¶
factory-server/config/application.yaml:
auth:
jwtSecret: "${OKTO_AUTH_JWT_SECRET}"
jwtIssuer: "okto-factory"
jwtAudience: "okto-edge"
tokenExpirationMs: 86400000 # 24h для пользовательских токенов
deviceEnrollmentKey: "${OKTO_ENROLLMENT_KEY}"
allowAutoEnrollment: true # false в high-security
firmware:
storageDir: "data/firmware"
maxArtifactSizeBytes: 268435456 # 256 MB
allowedChannels: ["stable","beta","canary"]
management:
defaultCommandTimeoutMs: 30000
maxGroupSize: 500
dashboardWsSubscriptionTimeoutMs: 5000
heartbeatIntervalSeconds: 25
Параметры можно переопределить переменными окружения с префиксом OKTO_:
OKTO_AUTH_DEVICEENROLLMENTKEY=xxxOKTO_MANAGEMENT_MAXGROUPSIZE=1000
17. Наблюдаемость и мониторинг¶
Метрики для алертинга¶
okto_devices_online {site="ru-01"} — живые WS-сессии
okto_cloud_sync_queue_size {status="DEAD_LETTER"}
okto_commands_total {type,status}
okto_firmware_deployments_total {status}
okto_device_heartbeat_lag_seconds {device} — max(now - last_heartbeat)
Рекомендованные алерты¶
| Алерт | Условие | Severity |
|---|---|---|
DevicesOfflineSurge |
delta(okto_devices_online[5m]) < -5 |
P1 |
CloudQueueDeadLetter |
okto_cloud_sync_queue_size{status="DEAD_LETTER"} > 0 |
P2 |
CommandTimeoutsHigh |
rate(okto_commands_total{status="TIMEOUT"}[5m]) > 0.1 |
P2 |
FirmwareDeployFailureRate |
sum(okto_firmware_deployments_total{status="FAILED"}) / sum(total) > 0.1 |
P2 |
DeviceHeartbeatStale |
okto_device_heartbeat_lag_seconds > 120 |
P3 |
См. OPERATIONS.ru.md для детальных runbook-ов.
18. Известные ограничения¶
- Один инстанс ЦЛС на площадку. Регистрация
DeviceConnectionRegistryв памяти не шардится. Горизонтальное масштабирование потребует внешнего координатора (Redis Pub/Sub / Postgres LISTEN/NOTIFY). push_configне persistent. In-memory store. Для durable —restart_service+ правка/etc/okto/application.yaml.- Device JWT в query-string. Проходит через access-логи proxy. Стрипайте
?token=…в конфиге proxy (пример для nginx:proxy_set_header X-Orig-Token $arg_token; proxy_pass …иlog_formatбез args). - Нет per-user revocation JWT. Logout убивает session row (для legacy), но JWT живёт до
exp. Прокрутauth.jwtSecret= revoke всех. - Прошивки без Ed25519-проверки по умолчанию. Включите в production, встроив trusted public key.
- Cloud auth token долгоживущий. При миграции на OAuth оберните
FactoryCloudClientрефрешером. - Логи устройств растут бесконтрольно. Добавьте ежедневный
DELETE FROM device_logs WHERE ts < NOW() - INTERVAL '30 days'.
19. Диагностика и ответы на частые проблемы¶
Устройство видно как OFFLINE, но edge работает¶
- Проверьте
connection_modeустройства — должно бытьVIA_LOCAL_SERVERдля WS-подключения. GET /api/v1/devices/connected— идентификатор есть в ответе? Если нет, WS-сессия не установлена.- Tail edge-лога: ищите
ServerConnectionService: Connecting to factory server WS. При reject → проверьте enrollment key и valid device JWT. curl -I https://<factory>/healthс edge-устройства — TLS и DNS ок?- Если WS живой, но
devices.last_heartbeatстарый — возможноServerConnectionServiceне шлётStatusEvent. Рестарт edge.
Все команды отваливаются по TIMEOUT¶
device_logsне появляются после dispatch →CommandHandlerServiceполучил команду, но не ответил. Обычно исключение в executor-е.device_logsне появляются совсем → WS разорван между dispatch и delivery. Добавьте реконнект-логирование.- Если
dispatched_at= null → команда не попала в WS (offline / backpressure).
Деплой прошивки SUCCESS, но устройство показывает старую версию¶
Edge только stages артефакт. Supervisor подменит JAR при следующем рестарте. Пошлите restart_service:
curl -X POST https://factory/api/v1/devices/edge-01/commands \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"command":{"type":"restart_service","id":"'$(uuidgen)'"},"timeoutMs":60000}'
sha256 mismatch при OTA¶
- Проверьте, что MIME artifact не модифицировался proxy-ем (gzip/br?). Добавьте
proxy_buffering offиproxy_max_temp_file_size 0. - Ручная проверка:
curl -L $ARTIFACT_URL -o fw.jar && sha256sum fw.jar— сравните сfirmware_releases.sha256.
Роль не пропускает команду, хотя всё настроено¶
GET /api/v1/auth/meвозвращает правильныйrole?- В
audit_logищитеaction=AUTH_ROLE_DENIED. - Токен свежий? Старый JWT мог быть выпущен до повышения роли.
«Device offline» хотя только что был онлайн¶
Вероятно WS-сессия упала, а last_heartbeat ещё не обновился:
registry.isOnline(deviceId)смотрит на carry-живую сессию, не на БД.- Запросите
GET /api/v1/devices/connected— актуальный срез. - Либо повторите dispatch через 3–5 с.
Обновлено: апрель 2026. Ответственные: engineering@okto.ru (код), ops@okto.ru (эксплуатация).