Перейти к содержанию

Управление парком устройств

Подробный справочник по удалённому управлению edge-устройствами с центрального локального сервера (далее — ЦЛС): протокол команд, WebSocket-канал, REST API, RBAC, прошивки, аудит, модель безопасности.

Аудитория: DevOps, инженеры сопровождения, системные интеграторы, авторы консолей управления. Связанные документы: ARCHITECTURE.ru.md, API_REFERENCE.ru.md, OPERATIONS.ru.md.


Содержание

  1. Архитектура управления
  2. Аутентификация и выдача токенов
  3. Регистрация (enrollment) устройства
  4. Каталог команд
  5. Отправка команды (REST)
  6. Массовые команды по группам
  7. История команд
  8. Стриминг логов
  9. Управление конфигурацией (push_config)
  10. Прошивки и OTA
  11. Группы устройств
  12. Журнал аудита
  13. Роли и разрешения (RBAC)
  14. Модель безопасности
  15. WS-протокол консоли
  16. Конфигурация ЦЛС для управления
  17. Наблюдаемость и мониторинг
  18. Известные ограничения
  19. Диагностика и ответы на частые проблемы

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):

  1. Заголовок X-Enrollment-Key сравнивается с auth.deviceEnrollmentKey в constant-time режиме. При несовпадении → 401 INVALID_ENROLLMENT_KEY.
  2. Если устройство уже зарегистрировано — сразу выпускается новый device JWT (для ротации).
  3. Если устройства нет и auth.allowAutoEnrollment=true — сервер делает INSERT INTO devices(identifier, name, company_id, production_line_id, version, status=OFFLINE) и возвращает токен.
  4. Если allowAutoEnrollment=false — ответ 403 DEVICE_NOT_REGISTERED. Устройство должно быть предварительно создано через POST /api/v1/devices администратором.

Ответ 200:

{ "success": true, "data": { "token": "eyJ…", "expiresAt": "2027-04-17T00:00:00Z" } }

Настройка симметричная

  • ЦЛС: 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

Расширение каталога

Чтобы добавить новую команду:

  1. В common/api/DeviceControl.kt добавьте @Serializable @SerialName("my_cmd") data class MyCmd(...) в sealed interface DeviceCommand.
  2. В edge/service/CommandHandlerService.kt добавьте ветку в handle() и реализуйте хэндлер.
  3. При необходимости — new REST endpoint в ServerManagementRoutes.kt для удобной отправки.
  4. Добавьте локализацию в management-dashboard/src/i18n/locales/*.json (key device.commands.my_cmd.*).
  5. Обновите таблицу Каталог команд и тесты.

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 }
}

Одна команда

GET /api/v1/devices/{id}/commands/{cmdId}

8. Стриминг логов

Pull

Команда pull_logs возвращает «хвост» лог-файла edge-устройства как серию LogLineEvent. Каждое событие фиксируется в device_logs:

INSERT INTO device_logs (device_id, command_id, ts, level, logger, line)
VALUES (?, ?, ?, ?, ?, ?);

Просмотр из консоли

GET /api/v1/devices/{id}/logs?limit=200&offset=0&level=WARN&since=2026-04-17T20:00:00Z

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():

  1. Парсит patchJsonMap<String, Any>.
  2. Мержит в InMemoryDeviceConfigStore.patch.
  3. Возвращает 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&notes=Hotfix&filename=edge-service.jar
Authorization: Bearer <userJwt>
Content-Type: application/octet-stream

<binary artifact>

Обработка (FirmwareService.storeRelease):

  1. Stream в data/firmware/<safeVersion>-<filename>.
  2. Считается SHA-256 на лету.
  3. INSERT INTO firmware_releases (id, version, channel, artifact_url, sha256, size_bytes, created_by, created_at).
  4. Возвращается FirmwareRelease (см. API_REFERENCE.ru.md).

10.2 Деплой

POST /api/v1/firmware/deployments
{
  "releaseId": "73e395e6-…",
  "deviceIds": ["edge-01", "edge-02"]
}

Альтернативно: "groupId": "grp-eu" для деплоя по группе.

Сервер:

  1. Для каждого targeted device создаёт firmware_deployments(status=PENDING).
  2. Отправляет UpdateFirmwareCmd { releaseId, url, sha256, version }.
  3. При получении CommandResult.success=truestatus=SUCCESS; иначе FAILED.

Ответ синхронно возвращает Map<deviceId, CommandResult>.

10.3 Поведение устройства

UpdateFirmwareExecutor:

  1. Скачивает url (GET с Authorization: Bearer \<deviceJwt>) в <okto.firmware.staging.dir>/edge-service-<version>.jar.part.
  2. Проверяет SHA-256; несовпадение → CommandResult.success=false, error="sha256 mismatch".
  3. Переименовывает в .jar.
  4. Запускает sudo systemctl start okto-edge-update (one-shot unit, запускает скрипт /usr/local/bin/okto-edge-swap-firmware).
  5. Скрипт:
  6. бэкапит старый edge-service.jar в edge-service.jar.bak,
  7. перемещает staged JAR на место,
  8. вызывает systemctl restart okto-edge.
  9. После рестарта новый процесс подключается через enrollment (старый token) и сообщает новую firmwareVersion в DeviceHelloMessage. ЦЛС апдейтит devices.firmware_version.

10.4 Откат

Быстрый откат — отправить UpdateFirmwareCmd с предыдущей версией (она сохранена в .bak, но swap-script перезапишет её только если SHA match). Проще — загрузите предыдущий JAR как новый релиз и деплойте его.

10.5 Броский релиз (signatures)

Поле signatureBase64 в FirmwareRelease зарезервировано под Ed25519-подпись. В дефолтной поставке проверка выключена. Для продакшна:

  1. Сгенерируйте ключевую пару ssh-keygen -t ed25519 -f okto-release.
  2. Подпишите JAR: openssl pkeyutl -sign -inkey okto-release -rawin -in edge-service-1.2.3.jar | base64.
  3. Передайте signature при uploading (query param signatureBase64=…).
  4. Встройте публичный ключ в ресурсы 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 для конкретных проверок.

Добавление новой роли

  1. В common/domain/Auth.kt добавьте значение в enum UserRole.
  2. Обновите requireRole / таблицу разрешений.
  3. В 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 var OKTO_AUTH_JWT_SECRET.
  • cloudSync.authToken — аналогично.
  • auth.deviceEnrollmentKey — аналогично.
  • Hoplite поддерживает ${ENV_VAR} подстановку в application.yaml.

15. WS-протокол консоли

Подключение:

wss://<factory>/ws/dashboard?token=<userJwt>

После open сервер ожидает subscription-сообщение в течение 5 секунд, иначе закрывает соединение. Минимально:

{ "type": "subscribe", "deviceIds": [], "eventTypes": [] }

Пустые массивы = получать всё.

Типы входящих событий

{"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"]}

Отписка

{"type":"unsubscribe"}

Сервер перестанет слать события, но соединение не закроется.

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=xxx
  • OKTO_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 работает

  1. Проверьте connection_mode устройства — должно быть VIA_LOCAL_SERVER для WS-подключения.
  2. GET /api/v1/devices/connected — идентификатор есть в ответе? Если нет, WS-сессия не установлена.
  3. Tail edge-лога: ищите ServerConnectionService: Connecting to factory server WS. При reject → проверьте enrollment key и valid device JWT.
  4. curl -I https://<factory>/health с edge-устройства — TLS и DNS ок?
  5. Если 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

  1. Проверьте, что MIME artifact не модифицировался proxy-ем (gzip/br?). Добавьте proxy_buffering off и proxy_max_temp_file_size 0.
  2. Ручная проверка: 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 (эксплуатация).