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
+7
View File
@@ -233,6 +233,13 @@ and `LogDump/` trees.
`collector.ReplayRedfishFromRawPayloads` (`internal/parser/vendors/dell/redfish_walk.go`), then
append-merged into the same `Hardware`/`Sensors`/`FRU`/`Events` slices the DCIM-XML path fills, so
the existing dedupe passes resolve any overlap in favor of DCIM-derived data. See ADL-048.
- Licenses (`hardware.licenses[]`, contract v2.12): the Redfish-walk replay reads the standard DMTF
`/redfish/v1/LicenseService/Licenses` collection (`internal/collector/redfish_replay_licenses.go`),
present on iDRAC10-generation firmware — feature-on-demand/advanced licenses such as
"iDRAC10 17G Enterprise License", "Secure Enterprise Key Manager". System-level licenses have no
`component_ref`; a license with `AuthorizationScope: "Device"` gets `component_ref` from
`Links.AuthorizedDevices`. Not sourced from DCIM-XML — only available on the Redfish-walk path.
See ADL-050.
---
+43
View File
@@ -1310,3 +1310,46 @@ given the field it needs from a re-imported export.
`parseUploadedSnapshot` must actually be populated by its `convert*FromDevices` function — the struct
declaring the field is not enough. `Storage` was the only category doing this correctly before this fix;
worth auditing PCIe/NIC/GPU conversion the same way if a similar round-trip gap is reported for them.
---
## ADL-050 — Added `hardware.licenses[]` collection/export (contract v2.12), Dell iDRAC10 first
**Date:** 2026-08-11
**Context:** Reanimator's hardware ingest contract added an optional `hardware.licenses[]` section in
v2.12 (2026-08-11) for software/firmware licenses and feature-on-demand activations (BMC advanced
licenses, vGPU, CPU FoD, RAID feature unlocks, etc). LOGPile's local copy of
`bible-local/docs/hardware-ingest-contract.md` was still v2.11 and was refreshed from
`reanimator/core/bible-local/docs/hardware-ingest-contract.md`. Scoped the first implementation to the
Dell iDRAC10 Redfish-walk path (`vendors/dell` + `vendors/redfishwalk`, see ADL-048/049), since that's
the only resolver currently producing a full captured Redfish tree with a `LicenseService` collection
in it — other vendor parsers have no comparable source for this data yet.
**Decision:**
- `models.License` (`internal/models/models.go`) added, mirroring the contract's field set 1:1
(`name`, `license_key`, `vendor`, `type`, `feature`, `component_ref`, `activated_at`, `expires_at`,
`present`, status fields). `HardwareConfig.Licenses []License` added.
- `internal/collector/redfish_replay_licenses.go`: `collectLicenses()` reads the standard DMTF
`/redfish/v1/LicenseService/Licenses` collection (`License.v1_x` schema) via the existing
`redfishSnapshotReader.getCollectionMembers` helper — no Dell-specific parsing needed, this is a
generic DMTF resource, so any future vendor whose Redfish walk includes it gets license collection
for free through `ReplayRedfishFromRawPayloads`. `AuthorizationScope: "Device"` licenses get
`component_ref` from `Links.AuthorizedDevices[0]`; `Service`-scoped licenses stay system-level (no
`component_ref`). `LicenseOrigin != "Installed"` maps to `Present: false`.
- `internal/parser/vendors/dell/redfish_walk.go`'s `mergeRedfishReplay` appends replayed `Licenses`
into `result.Hardware.Licenses` like every other category.
- `internal/exporter/reanimator_converter.go`: `convertLicenses` + `dedupeLicenses` (dedup key:
`license_key`, falling back to `component_ref|name`) added, wired into `ConvertToReanimator`
directly from `hw.Licenses` — licenses don't go through the canonical-devices merge/dedup pipeline
used for PCIe/GPU/NIC, since they carry no physical identity to merge on. `ReanimatorLicense.Present`
is set on every emitted record (learned from ADL-049 — declaring the field isn't enough, it must
actually be populated for the reanimator round-trip to survive).
- `internal/chart/viewer/render.go`: added a `licenses` section (between `power_supplies` and
`sensors`) so licenses show up in the `/chart/current` web view like every other hardware category.
**Consequences:**
- Verified end-to-end on the PowerEdge R7715 (1TVFYL4) TSR archive: 3 licenses extracted from
`redfishidracwalk.tar.gz` ("Secure Enterprise Key Manager", "Secured Component Verification",
"iDRAC10 17G Enterprise License"), all system-level (`AuthorizationScope: "Service"`), correctly
exported with `present: true` and visible in the live `/chart/current` page.
- No other vendor parser populates `Hardware.Licenses` yet. If a future TSR/log source carries license
data outside a Redfish walk (e.g. embedded in a vendor-specific XML/JSON file), it needs its own
parsing — `collectLicenses()` only covers the generic Redfish `LicenseService` path.
+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",