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:
co-authored by
Claude Sonnet 5
parent
eb6cc207ce
commit
eaaf5c09d3
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user