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
|
`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
|
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.
|
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
|
`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;
|
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.
|
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
|
title: Hardware Ingest JSON Contract
|
||||||
version: "2.11"
|
version: "2.12"
|
||||||
updated: "2026-06-19"
|
updated: "2026-08-11"
|
||||||
maintainer: Reanimator Core
|
maintainer: Reanimator Core
|
||||||
audience: external-integrators, ai-agents
|
audience: external-integrators, ai-agents
|
||||||
language: ru
|
language: ru
|
||||||
@@ -9,7 +9,7 @@ language: ru
|
|||||||
|
|
||||||
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
|
# Интеграция с Reanimator: контракт JSON-импорта аппаратного обеспечения
|
||||||
|
|
||||||
Версия: **2.11** · Дата: **2026-06-19**
|
Версия: **2.12** · Дата: **2026-08-11**
|
||||||
|
|
||||||
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
|
Документ описывает формат JSON для передачи данных об аппаратном обеспечении серверов в систему **Reanimator** (управление жизненным циклом аппаратного обеспечения).
|
||||||
Предназначен для разработчиков смежных систем (Redfish-коллекторов, агентов мониторинга, CMDB-экспортёров) и может быть включён в документацию интегрируемых проектов.
|
Предназначен для разработчиков смежных систем (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.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.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 |
|
| 2.9 | 2026-03-19 | Добавлена необязательная секция `hardware.platform_config` — произвольный объект с настройками платформы (BIOS/Redfish); хранится как latest-snapshot per machine |
|
||||||
@@ -44,6 +45,11 @@ language: ru
|
|||||||
2. **Идемпотентность** — повторная отправка идентичного payload не создаёт дублей (дедупликация по хешу).
|
2. **Идемпотентность** — повторная отправка идентичного payload не создаёт дублей (дедупликация по хешу).
|
||||||
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
|
3. **Частичность** — можно передавать только те секции, данные по которым доступны. Пустой массив и отсутствие секции эквивалентны.
|
||||||
4. **Строгая схема** — endpoint использует строгий JSON-декодер; неизвестные поля приводят к `400 Bad Request`.
|
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 и др.).
|
5. **Event-driven** — импорт создаёт события в timeline (LOG_COLLECTED, INSTALLED, REMOVED, FIRMWARE_CHANGED и др.).
|
||||||
6. **Без синтеза со стороны интегратора** — сборщик передаёт только фактически собранные значения. Нельзя придумывать `serial_number`, `component_ref`, `message`, `message_id` или другие идентификаторы/атрибуты, если источник их не предоставил или парсер не смог их надёжно извлечь.
|
6. **Без синтеза со стороны интегратора** — сборщик передаёт только фактически собранные значения. Нельзя придумывать `serial_number`, `component_ref`, `message`, `message_id` или другие идентификаторы/атрибуты, если источник их не предоставил или парсер не смог их надёжно извлечь.
|
||||||
|
|
||||||
@@ -60,7 +66,7 @@ Content-Type: application/json
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"job_id": "job_01J..."
|
"job_id": "job-1784799375786854143"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -69,12 +75,31 @@ Content-Type: application/json
|
|||||||
GET /ingest/hardware/jobs/{job_id}
|
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
|
```json
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"bundle_id": "lb_01J...",
|
"bundle_id": "lb_01J...",
|
||||||
"asset_id": "mach_01J...",
|
"asset_id": "ME-0000090",
|
||||||
"collected_at": "2026-02-10T15:30:00Z",
|
"collected_at": "2026-02-10T15:30:00Z",
|
||||||
"duplicate": false,
|
"duplicate": false,
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -89,7 +114,7 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ответ при дубликате:**
|
**`result` при дубликате:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -137,7 +162,8 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
"power_supplies": [ ... ],
|
"power_supplies": [ ... ],
|
||||||
"sensors": { ... },
|
"sensors": { ... },
|
||||||
"event_logs": [ ... ],
|
"event_logs": [ ... ],
|
||||||
"platform_config": { ... }
|
"platform_config": { ... },
|
||||||
|
"licenses": [ ... ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -475,6 +501,14 @@ GET /ingest/hardware/jobs/{job_id}
|
|||||||
|
|
||||||
**Ключ дедупликации:** `(pcie_devices[].slot, sfp_modules[].port)`.
|
**Ключ дедупликации:** `(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:**
|
**Правила ingest:**
|
||||||
- При каждом импорте — полная замена `sfp_modules[]` для данного `pcie_devices[].slot` (upsert всего массива целиком).
|
- При каждом импорте — полная замена `sfp_modules[]` для данного `pcie_devices[].slot` (upsert всего массива целиком).
|
||||||
- Если `sfp_modules` отсутствует или `null` — существующие данные SFP не трогать.
|
- Если `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": {
|
"sensors": {
|
||||||
"fans": [
|
"fans": [
|
||||||
{ "name": "FAN1", "location": "Front", "rpm": 4200, "status": "OK" }
|
{ "name": "FAN1", "rpm": 4200, "status": "OK" }
|
||||||
],
|
],
|
||||||
"power": [
|
"power": [
|
||||||
{ "name": "12V Rail", "voltage_v": 12.06, "status": "OK" }
|
{ "name": "12V Rail", "voltage_v": 12.06, "status": "OK" }
|
||||||
@@ -908,6 +1022,15 @@ PSU без `serial_number` игнорируется.
|
|||||||
{ "name": "System Humidity", "value": 38.5, "unit": "%" }
|
{ "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": {
|
"platform_config": {
|
||||||
"SecureBoot": "Enabled",
|
"SecureBoot": "Enabled",
|
||||||
"BiosVersion": "06.08.05",
|
"BiosVersion": "06.08.05",
|
||||||
|
|||||||
+1
-1
Submodule internal/chart updated: 8c80591531...9552a483ae
@@ -79,6 +79,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
emit(Progress{Status: "running", Progress: 80, Message: "Redfish snapshot: replay network/BMC..."})
|
emit(Progress{Status: "running", Progress: 80, Message: "Redfish snapshot: replay network/BMC..."})
|
||||||
}
|
}
|
||||||
psus := r.collectPSUs(chassisPaths)
|
psus := r.collectPSUs(chassisPaths)
|
||||||
|
licenses := r.collectLicenses()
|
||||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||||
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
||||||
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
||||||
@@ -126,6 +127,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
PCIeDevices: pcieDevices,
|
PCIeDevices: pcieDevices,
|
||||||
GPUs: gpus,
|
GPUs: gpus,
|
||||||
PowerSupply: psus,
|
PowerSupply: psus,
|
||||||
|
Licenses: licenses,
|
||||||
NetworkAdapters: nics,
|
NetworkAdapters: nics,
|
||||||
Firmware: firmware,
|
Firmware: firmware,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// collectLicenses reads the standard DMTF LicenseService/Licenses collection
|
||||||
|
// (Redfish License.v1_x schema). Seen first on Dell iDRAC10-generation
|
||||||
|
// firmware, which exposes it alongside the legacy Oem/Dell license resources.
|
||||||
|
// Entries are system-level licenses unless AuthorizationScope is "Device",
|
||||||
|
// in which case Links.AuthorizedDevices identifies the licensed component.
|
||||||
|
func (r redfishSnapshotReader) collectLicenses() []models.License {
|
||||||
|
memberDocs, err := r.getCollectionMembers("/redfish/v1/LicenseService/Licenses")
|
||||||
|
if err != nil || len(memberDocs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]models.License, 0, len(memberDocs))
|
||||||
|
for _, doc := range memberDocs {
|
||||||
|
lic, ok := parseRedfishLicense(doc)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, lic)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedfishLicense(doc map[string]interface{}) (models.License, bool) {
|
||||||
|
name := strings.TrimSpace(firstNonEmpty(
|
||||||
|
asString(doc["Description"]),
|
||||||
|
asString(doc["Name"]),
|
||||||
|
asString(doc["Id"]),
|
||||||
|
))
|
||||||
|
if name == "" {
|
||||||
|
return models.License{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
origin := strings.ToLower(strings.TrimSpace(asString(doc["LicenseOrigin"])))
|
||||||
|
present := origin == "" || origin == "installed"
|
||||||
|
|
||||||
|
lic := models.License{
|
||||||
|
Name: name,
|
||||||
|
LicenseKey: strings.TrimSpace(asString(doc["EntitlementId"])),
|
||||||
|
Type: strings.TrimSpace(asString(doc["LicenseType"])),
|
||||||
|
ComponentRef: redfishLicenseComponentRef(doc),
|
||||||
|
Present: present,
|
||||||
|
Status: mapStatus(doc["Status"]),
|
||||||
|
ActivatedAt: parseRedfishLicenseTime(doc["InstallDate"]),
|
||||||
|
ExpiresAt: parseRedfishLicenseTime(doc["ExpirationDate"]),
|
||||||
|
}
|
||||||
|
return lic, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedfishLicenseTime(v interface{}) time.Time {
|
||||||
|
raw := strings.TrimSpace(asString(v))
|
||||||
|
if raw == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
for _, layout := range []string{time.RFC3339, time.RFC3339Nano} {
|
||||||
|
if ts, err := time.Parse(layout, raw); err == nil {
|
||||||
|
return ts.UTC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redfishLicenseComponentRef(doc map[string]interface{}) string {
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(asString(doc["AuthorizationScope"])), "Device") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
links, ok := doc["Links"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
devices, ok := links["AuthorizedDevices"].([]interface{})
|
||||||
|
if !ok || len(devices) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
first, ok := devices[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(asString(first["@odata.id"]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRedfishLicense(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"@odata.id": "/redfish/v1/LicenseService/Licenses/FD00000043163704",
|
||||||
|
"Description": "iDRAC10 17G Enterprise License",
|
||||||
|
"EntitlementId": "FD00000043163704",
|
||||||
|
"LicenseType": "Production",
|
||||||
|
"LicenseOrigin": "Installed",
|
||||||
|
"AuthorizationScope": "Service",
|
||||||
|
"InstallDate": nil,
|
||||||
|
"ExpirationDate": nil,
|
||||||
|
"Links": map[string]interface{}{},
|
||||||
|
"Status": map[string]interface{}{
|
||||||
|
"Health": "OK",
|
||||||
|
"State": "Enabled",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lic, ok := parseRedfishLicense(doc)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected license to parse")
|
||||||
|
}
|
||||||
|
if lic.Name != "iDRAC10 17G Enterprise License" {
|
||||||
|
t.Errorf("Name = %q, want %q", lic.Name, "iDRAC10 17G Enterprise License")
|
||||||
|
}
|
||||||
|
if lic.LicenseKey != "FD00000043163704" {
|
||||||
|
t.Errorf("LicenseKey = %q, want %q", lic.LicenseKey, "FD00000043163704")
|
||||||
|
}
|
||||||
|
if lic.Type != "Production" {
|
||||||
|
t.Errorf("Type = %q, want %q", lic.Type, "Production")
|
||||||
|
}
|
||||||
|
if !lic.Present {
|
||||||
|
t.Error("expected Present = true for LicenseOrigin=Installed")
|
||||||
|
}
|
||||||
|
if lic.ComponentRef != "" {
|
||||||
|
t.Errorf("ComponentRef = %q, want empty for Service-scoped license", lic.ComponentRef)
|
||||||
|
}
|
||||||
|
if lic.Status != "OK" {
|
||||||
|
t.Errorf("Status = %q, want %q", lic.Status, "OK")
|
||||||
|
}
|
||||||
|
if !lic.ActivatedAt.IsZero() {
|
||||||
|
t.Errorf("expected zero ActivatedAt for null InstallDate, got %v", lic.ActivatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRedfishLicense_DeviceScoped(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"Description": "NVIDIA vGPU",
|
||||||
|
"EntitlementId": "ABC123",
|
||||||
|
"AuthorizationScope": "Device",
|
||||||
|
"LicenseOrigin": "Installed",
|
||||||
|
"InstallDate": "2025-06-01T00:00:00Z",
|
||||||
|
"ExpirationDate": "2026-12-31T23:59:59Z",
|
||||||
|
"Links": map[string]interface{}{
|
||||||
|
"AuthorizedDevices": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/0-193-0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Status": map[string]interface{}{"Health": "Warning"},
|
||||||
|
}
|
||||||
|
|
||||||
|
lic, ok := parseRedfishLicense(doc)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected license to parse")
|
||||||
|
}
|
||||||
|
if lic.ComponentRef != "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/0-193-0" {
|
||||||
|
t.Errorf("ComponentRef = %q, want the linked device path", lic.ComponentRef)
|
||||||
|
}
|
||||||
|
if lic.Status != "Warning" {
|
||||||
|
t.Errorf("Status = %q, want %q", lic.Status, "Warning")
|
||||||
|
}
|
||||||
|
if lic.ActivatedAt.IsZero() || lic.ExpiresAt.IsZero() {
|
||||||
|
t.Errorf("expected non-zero ActivatedAt/ExpiresAt, got %v / %v", lic.ActivatedAt, lic.ExpiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRedfishLicense_NotInstalledIsNotPresent(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"Description": "Available Feature",
|
||||||
|
"LicenseOrigin": "NotInstalled",
|
||||||
|
}
|
||||||
|
lic, ok := parseRedfishLicense(doc)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected license to parse")
|
||||||
|
}
|
||||||
|
if lic.Present {
|
||||||
|
t.Error("expected Present = false for LicenseOrigin=NotInstalled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRedfishLicense_MissingNameSkipped(t *testing.T) {
|
||||||
|
if _, ok := parseRedfishLicense(map[string]interface{}{}); ok {
|
||||||
|
t.Error("expected license without any name field to be skipped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplayRedfishFromRawPayloads_CollectsLicenses(t *testing.T) {
|
||||||
|
rawPayloads := map[string]any{
|
||||||
|
"redfish_tree": map[string]interface{}{
|
||||||
|
"/redfish/v1": map[string]interface{}{},
|
||||||
|
"/redfish/v1/Systems": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems/1": map[string]interface{}{"Id": "1"},
|
||||||
|
"/redfish/v1/LicenseService/Licenses": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/LicenseService/Licenses/A"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/LicenseService/Licenses/A": map[string]interface{}{
|
||||||
|
"Description": "iDRAC10 17G Enterprise License",
|
||||||
|
"EntitlementId": "A",
|
||||||
|
"LicenseType": "Production",
|
||||||
|
"LicenseOrigin": "Installed",
|
||||||
|
"AuthorizationScope": "Service",
|
||||||
|
"Status": map[string]interface{}{"Health": "OK"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(result.Hardware.Licenses) != 1 {
|
||||||
|
t.Fatalf("expected 1 license, got %+v", result.Hardware.Licenses)
|
||||||
|
}
|
||||||
|
if result.Hardware.Licenses[0].Name != "iDRAC10 17G Enterprise License" {
|
||||||
|
t.Errorf("license name = %q, want %q", result.Hardware.Licenses[0].Name, "iDRAC10 17G Enterprise License")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro
|
|||||||
Sensors: convertSensors(result.Sensors),
|
Sensors: convertSensors(result.Sensors),
|
||||||
BMCEventSummary: buildBMCEventSummary(result.Events, collectedAt),
|
BMCEventSummary: buildBMCEventSummary(result.Events, collectedAt),
|
||||||
EventLogs: convertEventLogs(result.Events, collectedAt),
|
EventLogs: convertEventLogs(result.Events, collectedAt),
|
||||||
|
Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1858,6 +1859,60 @@ func buildStatusMeta(
|
|||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertLicenses converts software/firmware licenses (BMC advanced licenses,
|
||||||
|
// feature-on-demand activations, vGPU, etc). Unlike PCIe/GPU/NIC, licenses do
|
||||||
|
// not go through the canonical devices merge/dedup pipeline: they carry no
|
||||||
|
// physical identity to merge on and are reported directly from the source.
|
||||||
|
func convertLicenses(licenses []models.License, collectedAt string) []ReanimatorLicense {
|
||||||
|
result := make([]ReanimatorLicense, 0, len(licenses))
|
||||||
|
for _, lic := range licenses {
|
||||||
|
name := strings.TrimSpace(lic.Name)
|
||||||
|
if name == "" || !lic.Present {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status := normalizeStatus(lic.Status, false)
|
||||||
|
meta := buildStatusMeta(status, lic.StatusCheckedAt, lic.StatusChangedAt, lic.StatusHistory, lic.ErrorDescription, collectedAt)
|
||||||
|
present := lic.Present
|
||||||
|
result = append(result, ReanimatorLicense{
|
||||||
|
Name: name,
|
||||||
|
LicenseKey: strings.TrimSpace(lic.LicenseKey),
|
||||||
|
Vendor: strings.TrimSpace(lic.Vendor),
|
||||||
|
Type: strings.TrimSpace(lic.Type),
|
||||||
|
Feature: strings.TrimSpace(lic.Feature),
|
||||||
|
ComponentRef: strings.TrimSpace(lic.ComponentRef),
|
||||||
|
ActivatedAt: formatOptionalRFC3339(&lic.ActivatedAt),
|
||||||
|
ExpiresAt: formatOptionalRFC3339(&lic.ExpiresAt),
|
||||||
|
Present: &present,
|
||||||
|
Status: status,
|
||||||
|
StatusCheckedAt: meta.StatusCheckedAt,
|
||||||
|
StatusChangedAt: meta.StatusChangedAt,
|
||||||
|
StatusHistory: meta.StatusHistory,
|
||||||
|
ErrorDescription: meta.ErrorDescription,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeLicenses(items []ReanimatorLicense) []ReanimatorLicense {
|
||||||
|
if len(items) < 2 {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(items))
|
||||||
|
result := make([]ReanimatorLicense, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(item.LicenseKey))
|
||||||
|
if key == "" {
|
||||||
|
key = strings.ToLower(strings.TrimSpace(item.ComponentRef)) + "|" + strings.ToLower(strings.TrimSpace(item.Name))
|
||||||
|
}
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func formatOptionalRFC3339(t *time.Time) string {
|
func formatOptionalRFC3339(t *time.Time) string {
|
||||||
if t == nil || t.IsZero() {
|
if t == nil || t.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -2077,3 +2077,75 @@ func TestConvertToReanimator_MemoryAndPSURoundTripSurvivesReimport(t *testing.T)
|
|||||||
t.Fatalf("power supplies did not survive reanimator round trip, got %+v", reconverted.Hardware.PowerSupplies)
|
t.Fatalf("power supplies did not survive reanimator round trip, got %+v", reconverted.Hardware.PowerSupplies)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConvertToReanimator_ExportsLicenses covers the hardware.licenses contract
|
||||||
|
// section (v2.12): system-level licenses (no component_ref) and component-scoped
|
||||||
|
// licenses, skipping records without a name and records the source marked absent.
|
||||||
|
func TestConvertToReanimator_ExportsLicenses(t *testing.T) {
|
||||||
|
activatedAt := time.Date(2025, 1, 10, 0, 0, 0, 0, time.UTC)
|
||||||
|
result := &models.AnalysisResult{
|
||||||
|
Filename: "test.zip",
|
||||||
|
CollectedAt: time.Date(2026, 8, 11, 8, 42, 18, 0, time.UTC),
|
||||||
|
Hardware: &models.HardwareConfig{
|
||||||
|
BoardInfo: models.BoardInfo{
|
||||||
|
Manufacturer: "Dell Inc.",
|
||||||
|
ProductName: "PowerEdge R7715",
|
||||||
|
SerialNumber: "1TVFYL4",
|
||||||
|
},
|
||||||
|
Licenses: []models.License{
|
||||||
|
{
|
||||||
|
Name: "iDRAC10 17G Enterprise License",
|
||||||
|
LicenseKey: "FD00000043163704",
|
||||||
|
Type: "Production",
|
||||||
|
Present: true,
|
||||||
|
Status: "OK",
|
||||||
|
ActivatedAt: activatedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "NVIDIA vGPU",
|
||||||
|
ComponentRef: "0000:3b:00.0",
|
||||||
|
Present: true,
|
||||||
|
Status: "Warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// No name: source didn't provide one, must not be synthesized/kept.
|
||||||
|
LicenseKey: "NONAME",
|
||||||
|
Present: true,
|
||||||
|
Status: "OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Not Actually Installed",
|
||||||
|
Present: false,
|
||||||
|
Status: "OK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exported, err := ConvertToReanimator(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(exported.Hardware.Licenses) != 2 {
|
||||||
|
t.Fatalf("expected 2 licenses (nameless and not-present filtered out), got %+v", exported.Hardware.Licenses)
|
||||||
|
}
|
||||||
|
|
||||||
|
system := exported.Hardware.Licenses[0]
|
||||||
|
if system.Name != "iDRAC10 17G Enterprise License" || system.ComponentRef != "" {
|
||||||
|
t.Errorf("system license = %+v, want system-level iDRAC10 entry", system)
|
||||||
|
}
|
||||||
|
if system.Present == nil || !*system.Present {
|
||||||
|
t.Errorf("expected system license present=true, got %+v", system.Present)
|
||||||
|
}
|
||||||
|
if system.ActivatedAt != "2025-01-10T00:00:00Z" {
|
||||||
|
t.Errorf("ActivatedAt = %q, want %q", system.ActivatedAt, "2025-01-10T00:00:00Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
scoped := exported.Hardware.Licenses[1]
|
||||||
|
if scoped.Name != "NVIDIA vGPU" || scoped.ComponentRef != "0000:3b:00.0" {
|
||||||
|
t.Errorf("component license = %+v, want vGPU entry scoped to 0000:3b:00.0", scoped)
|
||||||
|
}
|
||||||
|
if scoped.Status != "Warning" {
|
||||||
|
t.Errorf("Status = %q, want %q", scoped.Status, "Warning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type ReanimatorHardware struct {
|
|||||||
BMCEventSummary []ReanimatorBMCEventRow `json:"bmc_event_summary,omitempty"`
|
BMCEventSummary []ReanimatorBMCEventRow `json:"bmc_event_summary,omitempty"`
|
||||||
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
|
EventLogs []ReanimatorEventLog `json:"event_logs,omitempty"`
|
||||||
PlatformConfig map[string]any `json:"platform_config,omitempty"`
|
PlatformConfig map[string]any `json:"platform_config,omitempty"`
|
||||||
|
Licenses []ReanimatorLicense `json:"licenses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReanimatorBMCEventRow is one row in the BMC critical/warning event summary table.
|
// ReanimatorBMCEventRow is one row in the BMC critical/warning event summary table.
|
||||||
@@ -218,6 +219,26 @@ type ReanimatorPSU struct {
|
|||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReanimatorLicense represents a software/firmware license or
|
||||||
|
// feature-on-demand activation (BMC advanced license, vGPU, CPU FoD, etc).
|
||||||
|
// ComponentRef is empty for system-level licenses.
|
||||||
|
type ReanimatorLicense struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
LicenseKey string `json:"license_key,omitempty"`
|
||||||
|
Vendor string `json:"vendor,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Feature string `json:"feature,omitempty"`
|
||||||
|
ComponentRef string `json:"component_ref,omitempty"`
|
||||||
|
ActivatedAt string `json:"activated_at,omitempty"`
|
||||||
|
ExpiresAt string `json:"expires_at,omitempty"`
|
||||||
|
Present *bool `json:"present,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
StatusCheckedAt string `json:"status_checked_at,omitempty"`
|
||||||
|
StatusChangedAt string `json:"status_changed_at,omitempty"`
|
||||||
|
StatusHistory []ReanimatorStatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ReanimatorEventLog struct {
|
type ReanimatorEventLog struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
EventTime string `json:"event_time,omitempty"`
|
EventTime string `json:"event_time,omitempty"`
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ type HardwareConfig struct {
|
|||||||
NetworkCards []NIC `json:"network_cards,omitempty"`
|
NetworkCards []NIC `json:"network_cards,omitempty"`
|
||||||
NetworkAdapters []NetworkAdapter `json:"network_adapters,omitempty"`
|
NetworkAdapters []NetworkAdapter `json:"network_adapters,omitempty"`
|
||||||
PowerSupply []PSU `json:"power_supplies,omitempty"`
|
PowerSupply []PSU `json:"power_supplies,omitempty"`
|
||||||
|
Licenses []License `json:"licenses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -357,6 +358,27 @@ type PSU struct {
|
|||||||
ErrorDescription string `json:"error_description,omitempty"`
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// License represents a software/firmware license or feature-on-demand
|
||||||
|
// activation (BMC advanced licenses, vGPU, RAID feature unlocks, CPU
|
||||||
|
// feature-on-demand, etc). ComponentRef is empty for system-level licenses.
|
||||||
|
type License struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
LicenseKey string `json:"license_key,omitempty"`
|
||||||
|
Vendor string `json:"vendor,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Feature string `json:"feature,omitempty"`
|
||||||
|
ComponentRef string `json:"component_ref,omitempty"`
|
||||||
|
ActivatedAt time.Time `json:"activated_at,omitempty"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
|
||||||
|
StatusCheckedAt *time.Time `json:"status_checked_at,omitempty"`
|
||||||
|
StatusChangedAt *time.Time `json:"status_changed_at,omitempty"`
|
||||||
|
StatusHistory []StatusHistoryEntry `json:"status_history,omitempty"`
|
||||||
|
ErrorDescription string `json:"error_description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// GPU represents a graphics processing unit
|
// GPU represents a graphics processing unit
|
||||||
type GPU struct {
|
type GPU struct {
|
||||||
Slot string `json:"slot"`
|
Slot string `json:"slot"`
|
||||||
|
|||||||
+1
@@ -78,6 +78,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
NetworkAdapters: make([]models.NetworkAdapter, 0),
|
NetworkAdapters: make([]models.NetworkAdapter, 0),
|
||||||
NetworkCards: make([]models.NIC, 0),
|
NetworkCards: make([]models.NIC, 0),
|
||||||
PowerSupply: make([]models.PSU, 0),
|
PowerSupply: make([]models.PSU, 0),
|
||||||
|
Licenses: make([]models.License, 0),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func mergeRedfishReplay(result *models.AnalysisResult, replayed *models.Analysis
|
|||||||
hw.GPUs = append(hw.GPUs, rhw.GPUs...)
|
hw.GPUs = append(hw.GPUs, rhw.GPUs...)
|
||||||
hw.NetworkAdapters = append(hw.NetworkAdapters, rhw.NetworkAdapters...)
|
hw.NetworkAdapters = append(hw.NetworkAdapters, rhw.NetworkAdapters...)
|
||||||
hw.PowerSupply = append(hw.PowerSupply, rhw.PowerSupply...)
|
hw.PowerSupply = append(hw.PowerSupply, rhw.PowerSupply...)
|
||||||
|
hw.Licenses = append(hw.Licenses, rhw.Licenses...)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Sensors = append(result.Sensors, replayed.Sensors...)
|
result.Sensors = append(result.Sensors, replayed.Sensors...)
|
||||||
|
|||||||
Reference in New Issue
Block a user