feat(hardware): collect and export licenses via Dell iDRAC10 Redfish walk

Implements the hardware.licenses[] contract section (v2.12, refreshed from
reanimator/core's hardware-ingest-contract.md — was v2.11 locally).

- models.License / HardwareConfig.Licenses mirror the contract field set.
- collector.collectLicenses() reads the standard DMTF
  /redfish/v1/LicenseService/Licenses collection during Redfish-walk replay;
  it's a generic DMTF resource, not Dell-specific, so any future vendor's
  Redfish walk gets license collection for free through
  ReplayRedfishFromRawPayloads.
- vendors/dell merges replayed Licenses like every other category.
- exporter.convertLicenses/dedupeLicenses wire hw.Licenses into the
  reanimator export directly (no canonical-devices merge — licenses have no
  physical identity to merge on), setting Present on every record from the
  start (per the ADL-049 round-trip lesson).
- chart viewer renders a licenses section in /chart/current.

Verified end-to-end on the PowerEdge R7715 (1TVFYL4) TSR: 3 system-level
licenses extracted and correctly exported/rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-11 11:48:21 +03:00
co-authored by Claude Sonnet 5
parent eb6cc207ce
commit eaaf5c09d3
13 changed files with 582 additions and 10 deletions
+132 -9
View File
@@ -1,7 +1,7 @@
---
title: Hardware Ingest JSON Contract
version: "2.11"
updated: "2026-06-19"
version: "2.12"
updated: "2026-08-11"
maintainer: Reanimator Core
audience: external-integrators, ai-agents
language: ru
@@ -9,7 +9,7 @@ language: ru
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
Версия: **2.11** · Дата: **2026-06-19**
Версия: **2.12** · Дата: **2026-08-11**
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
Предназначен для разработчиков смежных систем (Redfish-коллекторов, агентов мониторинга, CMDB-экспортёров) и может быть включён в документацию интегрируемых проектов.
@@ -22,6 +22,7 @@ language: ru
| Версия | Дата | Изменения |
|--------|------|-----------|
| 2.12 | 2026-08-11 | Добавлена необязательная секция `hardware.licenses[]` для лицензий на ПО/прошивку и связанного софтверного функционала (feature-on-demand, vGPU/iDRAC/iLO-style advanced-лицензии и т.п.). Лицензия может быть системной (без `component_ref`) либо привязанной к конкретному компоненту (`component_ref`). Секция использует те же общие поля статуса/истории, что и остальные компонентные секции |
| 2.11 | 2026-06-19 | В `pcie_devices[]` добавлен необязательный массив `sfp_modules[]` с идентификацией и DOM telemetry SFP/QSFP-модулей. Скалярные поля `sfp_temperature_c` / `sfp_tx_power_dbm` / `sfp_rx_power_dbm` / `sfp_voltage_v` / `sfp_bias_ma` помечены как deprecated (принимаются, но `sfp_modules[]` имеет приоритет) |
| 2.10 | 2026-04-29 | Для `hardware.storage[]` добавлены необязательные числовые поля `logical_block_size_bytes`, `physical_block_size_bytes`, `metadata_bytes_per_block` для нормализованного описания формата блока накопителя |
| 2.9 | 2026-03-19 | Добавлена необязательная секция `hardware.platform_config` — произвольный объект с настройками платформы (BIOS/Redfish); хранится как latest-snapshot per machine |
@@ -44,6 +45,11 @@ language: ru
2. **Идемпотентность** — повторная отправка идентичного payload не создаёт дублей (дедупликация по хешу).
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
4. **Строгая схема** — endpoint использует строгий JSON-декодер; неизвестные поля приводят к `400 Bad Request`.
> **Известное расхождение (2026-07-28):** на `POST /ingest/hardware` строгий декодер
> (`DisallowUnknownFields`) пока не включён — неизвестные поля сейчас тихо
> отбрасываются, а не отклоняются `400`. Включение отложено намеренно: рискованно
> для внешних интеграторов без предварительного аудита реальных payload на
> недокументированные поля. См. `bible-local/decisions/2026-07-28-ingest-contract-audit.md`.
5. **Event-driven** — импорт создаёт события в timeline (LOG_COLLECTED, INSTALLED, REMOVED, FIRMWARE_CHANGED и др.).
6. **Без синтеза со стороны интегратора** — сборщик передаёт только фактически собранные значения. Нельзя придумывать `serial_number`, `component_ref`, `message`, `message_id` или другие идентификаторы/атрибуты, если источник их не предоставил или парсер не смог их надёжно извлечь.
@@ -60,7 +66,7 @@ Content-Type: application/json
```json
{
"status": "accepted",
"job_id": "job_01J..."
"job_id": "job-1784799375786854143"
}
```
@@ -69,12 +75,31 @@ Content-Type: application/json
GET /ingest/hardware/jobs/{job_id}
```
**Ответ при успехе задачи:**
**Форма ответа поллинга**`job.status` проходит `queued` -> `running` -> `success`/`failed`; пока задача не завершена, `result`/`job.result` отсутствуют:
```json
{
"status": "ok",
"job": {
"id": "job-1784799375786854143",
"status": "success",
"filename": "21D634101",
"created_at": "2026-02-10T15:30:00Z",
"started_at": "2026-02-10T15:30:00Z",
"finished_at": "2026-02-10T15:30:01Z",
"result_code": 201,
"result": { "...": "см. ниже" }
},
"result_code": 201,
"result": { "...": "см. ниже" }
}
```
**`result` при успехе (result_code 201, или 200 для дубликата):**
```json
{
"status": "success",
"bundle_id": "lb_01J...",
"asset_id": "mach_01J...",
"asset_id": "ME-0000090",
"collected_at": "2026-02-10T15:30:00Z",
"duplicate": false,
"summary": {
@@ -89,7 +114,7 @@ GET /ingest/hardware/jobs/{job_id}
}
```
**Ответ при дубликате:**
**`result` при дубликате:**
```json
{
"status": "success",
@@ -137,7 +162,8 @@ GET /ingest/hardware/jobs/{job_id}
"power_supplies": [ ... ],
"sensors": { ... },
"event_logs": [ ... ],
"platform_config": { ... }
"platform_config": { ... },
"licenses": [ ... ]
}
}
```
@@ -475,6 +501,14 @@ GET /ingest/hardware/jobs/{job_id}
**Ключ дедупликации:** `(pcie_devices[].slot, sfp_modules[].port)`.
> **Известное ограничение (2026-07-28):** `sfp_modules[]` не имеет отдельной
> history-first проекции — "предыдущее" состояние читается из `observations`
> (raw ingest trace) исключительно для детектирования замены модуля и генерации
> события `COMPONENT_CHANGED`. Нет UI/API, читающего текущий список SFP-модулей.
> Использование `observations` здесь для сравнения — нарушение общего архитектурного
> правила «observations не источник истины для текущего состояния» (см.
> `runtime-flows.md`). См. `bible-local/decisions/2026-07-28-ingest-contract-audit.md`.
**Правила ingest:**
- При каждом импорте — полная замена `sfp_modules[]` для данного `pcie_devices[].slot` (upsert всего массива целиком).
- Если `sfp_modules` отсутствует или `null` — существующие данные SFP не трогать.
@@ -758,6 +792,86 @@ PSU без `serial_number` игнорируется.
---
## Секция licenses
Лицензии на ПО/прошивку и связанный с ними софтверный функционал: feature-on-demand активации (например, Intel FoD), advanced-лицензии BMC/удалённого управления (iDRAC/iLO Advanced), vGPU-лицензии, лицензии RAID-контроллеров (RAID60, кэширование и т.п.) и подобные программные разблокировки возможностей.
Секция необязательная. Лицензия может относиться:
- **к системе/серверу в целом** — `component_ref` не передаётся (например, лицензия BMC/redfish-сервиса);
- **к конкретному установленному компоненту** — `component_ref` указывает на этот компонент (см. правила ниже).
| Поле | Тип | Обязательно | Описание |
|------|-----|-------------|----------|
| `name` | string | **да** | Название лицензии/фичи, например `iDRAC9 Enterprise`, `Intel Feature-on-Demand: AVX-512-SP`, `NVIDIA vGPU` |
| `license_key` | string | нет | Ключ/идентификатор лицензии из источника (может быть частично маскирован источником) |
| `vendor` | string | нет | Производитель/издатель лицензии |
| `type` | string | нет | Тип лицензии: `Perpetual`, `Subscription`, `Trial`, `FeatureOnDemand`, `NodeLocked` и т.п. Список открытый |
| `feature` | string | нет | Конкретная функция/возможность, которую отпирает лицензия (софтверный функционал), например `AVX-512`, `RAID60`, `NVMe Caching` |
| `component_ref` | string | нет | Ссылка на компонент (slot/serial), к которому привязана лицензия; отсутствие поля = лицензия системного уровня |
| `activated_at` | string RFC3339 | нет | Время активации лицензии |
| `expires_at` | string RFC3339 | нет | Время истечения лицензии, если применимо |
| `present` | bool | нет | Наличие лицензии (по умолчанию `true`) |
| + общие поля статуса | | | см. раздел «Общие поля статуса компонентов» выше (`status`, `status_checked_at`, `status_changed_at`, `status_history`, `error_description`) |
Запись без `name` игнорируется.
**Значения `status` для лицензий** (тот же механизм status/status_history, что и у прочих компонентных секций, со своей семантикой):
| Значение | Смысл для лицензии |
|----------|---------------------|
| `OK` | Лицензия активна и валидна |
| `Warning` | Лицензия триальная либо истекает в ближайшее время |
| `Critical` | Лицензия просрочена, отозвана или невалидна |
| `Unknown` | Статус лицензии не удалось определить |
| `Empty` | Лицензия не обнаружена/не установлена — запись не создаётся |
Изменение статуса лицензии создаёт те же события, что и для остальных компонентов (`COMPONENT_WARNING`, `COMPONENT_FAILED` + запись в `failure_events` для `Critical`, `COMPONENT_UNKNOWN`) — см. «Обработка статусов компонентов» ниже.
**Генерация `vendor_serial` (ключ дедупликации) при отсутствии `license_key`:**
`{component_ref или board_serial}-LIC-{нормализованное имя лицензии}`.
Если и `license_key`, и сгенерированный ключ совпадают у двух записей в одном payload — это трактуется как повторная передача одной и той же лицензии (не ошибка).
**Правила ingest:**
- Как и другие компонентные секции, `licenses[]` — snapshot: лицензия, ранее переданная для сервера, но отсутствующая в новом payload, считается снятой/отозванной (аналогично `REMOVED` для физических компонентов).
- Интегратор не должен придумывать `license_key`, `name` или `expires_at`, если источник их не предоставил — см. общее правило «Без синтеза со стороны интегратора».
```json
"licenses": [
{
"name": "iDRAC9 Enterprise",
"vendor": "Dell",
"type": "Perpetual",
"feature": "Remote Console + vFlash + BMC OS-to-iDRAC Pass-through",
"license_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
"activated_at": "2025-01-10T00:00:00Z",
"status": "OK"
},
{
"name": "Intel Feature-on-Demand: AVX-512-SP",
"vendor": "Intel",
"type": "FeatureOnDemand",
"feature": "AVX-512",
"component_ref": "CPU0",
"status": "OK"
},
{
"name": "NVIDIA vGPU",
"vendor": "NVIDIA",
"type": "Subscription",
"component_ref": "0000:3b:00.0",
"expires_at": "2026-12-31T23:59:59Z",
"status": "Warning",
"status_history": [
{ "status": "OK", "changed_at": "2025-06-01T00:00:00Z" },
{ "status": "Warning", "changed_at": "2026-08-01T00:00:00Z", "details": "License expires in 30 days" }
]
}
]
```
---
## Обработка статусов компонентов
| Статус | Поведение |
@@ -896,7 +1010,7 @@ PSU без `serial_number` игнорируется.
],
"sensors": {
"fans": [
{ "name": "FAN1", "location": "Front", "rpm": 4200, "status": "OK" }
{ "name": "FAN1", "rpm": 4200, "status": "OK" }
],
"power": [
{ "name": "12V Rail", "voltage_v": 12.06, "status": "OK" }
@@ -908,6 +1022,15 @@ PSU без `serial_number` игнорируется.
{ "name": "System Humidity", "value": 38.5, "unit": "%" }
]
},
"licenses": [
{
"name": "iDRAC9 Enterprise",
"vendor": "Dell",
"type": "Perpetual",
"feature": "Remote Console + vFlash + BMC OS-to-iDRAC Pass-through",
"status": "OK"
}
],
"platform_config": {
"SecureBoot": "Enabled",
"BiosVersion": "06.08.05",